Home [데브코스] 9주차 - DeepLearning Numpy & Matplotlib
Post
Cancel

[데브코스] 9주차 - DeepLearning Numpy & Matplotlib


Numpy

Numpy 설치 및 불러오기

1
pip install numpy
1
import numpy as np


파이썬의 리스트는 머신러닝에서 가장 많이 사용되는 구조 중 하나일 것이다. 그러나 이 리스트는 연산 속도가 느리다. 그래서 연산을 효과적으로 할 수 있도록 하기 위해 만든 것이 Numpy이다.


Numpy 연산 속도

numpy와 list의 연산 속도를 비교해보고자 한다.

1
2
3
4
5
6
7
import numpy as np

L = range(1000)
%timeit [i**2 for i in L]

N = np.arange(1000)
%timeit N**2
1
2
1000 loops, best of 5: 264 µs per loop
1000000 loops, best of 5: 1.41 µs per loop

이 때 %timeit은 뒤의 연산에 대한 속도를 측정해준다. 시간을 보게 되면 리스트는 1000번의 루프동안 264µs 이지만, numpy의 경우 1000000번의 루프동안 1.41µs밖에 안걸린다. 동일한 루프 동안 걸리는 시간이 18만 배 정도가 차이난다.

지금은 단순히 1차원 배열이지만, 이것이 2차원, 3차원이 되면 더더욱 차이가 많이 나게 될 것이다.


리스트를 numpy로 직접 변환할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> li = [1,2,3]
> ar = np.array([1,2,3])
> arr = np.array(li)

> print(li,"\n",type(li))
[1, 2, 3] 
 <class 'list'>

> print(ar,"\n",type(ar))
[1 2 3] 
 <class 'numpy.ndarray'>

> print(arr,"\n",type(arr))
[1 2 3] 
 <class 'numpy.ndarray'>


Numpy 연산

vector와 scalar 연산

벡터의 각 원소에 대해 연산을 진행해보자.

\[y = \left( \begin{matrix} 1 \\ 3 \\ 5 \end{matrix} \right) \quad z = 5\]
1
2
x = np.array([1,2,3])
c = 5

x각각의 원소에 c를 더하기/곱하기/나누기 등을 해주자.


1
2
3
4
5
>print("더하기 : {}\n빼기 : {}\n곱하기 : {}\n나누기 : {}".format(x+c,x-c,x*c,x/c))
더하기 : [6 7 8]
빼기 : [-4 -3 -2]
곱하기 : [ 5 10 15]
나누기 : [0.2 0.4 0.6]

vector와 vector 연산

벡터끼리는 같은 인덱스끼리 연산이 진행된다.


\[y = \left( \begin{matrix} 1 \\ 3 \\ 5 \end{matrix} \right) \quad z = \left( \begin{matrix} 2 \\ 9 \\ 20 \end{matrix} \right)\]


1
2
3
4
5
6
7
8
> y = np.array([1,3,5])
> z = np.array([2,9,20])

> print("더하기 : {}\n빼기 : {}\n곱하기 : {}\n나누기 : {}".format(y+z,y-z,y*z,y/z))
더하기 : [ 3 12 25]
빼기 : [ -1  -6 -15]
곱하기 : [  2  27 100]
나누기 : [0.5        0.33333333 0.25      ]


numpy 인덱싱

list에서는 2차원 배열의 경우 k[a][b] 와 같이 인덱싱했다. numpy에서는 이와 유사하게 [a,b]로 인덱싱한다. 차이점은 numpy의 경우 ,를 사용한다.

1
2
3
> l = np.array([[1,2,3],[4,5,6]])
> l[1,2]
6


slicing(:) 하는 방법도 list와 동일하다.

1
2
3
> l[0:2,1:3]
array([[2, 3],
       [5, 6]])


array의 broadcasting

numpy에는 broadcasting이라는 특수한 기능이 있다. broadcasting이란 피연산자가 연산이 가능하도록 변환이 가능한 경우 변환하여 연산해주는 기능을 말한다. MxN 과 Mx1 을 연산하고자 한다면, 선형 대수 연산에서는 계산이 불가능하다.

그러나 이 때는 Mx1 배열을 복사해서 N개를 다 연산해준다.


예를 들어,

\[x = \left( \begin{matrix} 1 \ 2 \ 3 \\ 4 \ 5 \ 6 \\ 7 \ 8 \ 9 \end{matrix} \right) \quad y = \left( \begin{matrix} 0 \\ 0 \\ 1 \end{matrix} \right)\]


이러한 배열이 있다. 원래 선형 대수 연산에서는 차원이 다르므로 계산이 불가능하지만, numpy에서는 y를 3x3 크기로 만들어 연산한다.


MxN, 1xN 에서도 동일하게 동작한다. 그렇다면 Mx1 과 1xN 의 연산은 어떻게 동작할까?

이 경우에는 Mx1 을 MxN으로, 1xN을 MxN으로 만들어서, MxN 두개의 행렬을 연산한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# M by N, M by 1
x1 = np.array([[1,2,3],[4,5,6],[7,8,9]])
y1 = np.array([0,1,0]) 

# 이 때, y는 행 벡터이다. M은 행을 나타내고 있으므로, y를 전치해줘야 한다.
# 전치하는 방법은 여러 가지가 있다.
y1 = y1[:, None]

# M by N, 1 by N
y2 = np.array([0,1,-1]) 

# M by 1, 1 by N
x3 = np.array([1,2,3])
x3 = x3[:,None]
y3 = np.array([2,0,-2])
1
2
3
4
5
6
7
8
9
10
11
12
> print(x1*y1, "\n\n", x1*y2, "\n\n", x3*y3)
[[0 0 0]
 [4 5 6]
 [0 0 0]] 

 [[ 0  2 -3]
 [ 0  5 -6]
 [ 0  8 -9]] 

 [[ 2  0 -2]
 [ 4  0 -4]
 [ 6  0 -6]]


Numpy 응용 - Linear Algebra with numpy

여러 형태의 행렬 생성 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 영행렬
> zero = np.zeros((3,3)) # zeros(dim)
> zero
[[0.,0.,0.],
 [0.,0.,0.],
 [0.,0.,0.]]

# 일행렬
> one = np.ones((3,3)) # ones(dim)
[[1.,1.,1.],
 [1.,1.,1.],
 [1.,1.,1.]]

# 대각행렬
> diag = np.diag((1,3,5)) # diag((numbers))
[[1,0,0],
 [0,3,0],
 [0,0,5]]

# 항등행렬
> eye = np.eye(2, dtype=int) # eye(dim, data type)
[[1,0],
 [0,1]]


행렬 곱/나누기

행렬간의 곱연산은 np.dot() 또는 @ 을 사용한다.

1
2
3
4
5
6
7
8
9
10
>mat1 = np.array([[1,4],[2,3]])
>mat2 = np.array([[7,9],[0,6]])

>mat1.dot(mat2)
array([[ 7, 33],
       [14, 36]])

>mat1 @ mat2
array([[ 7, 33],
       [14, 36]])


트레이스(trace)

트레이스란 main diagnoal의 합을 말한다. 즉, 배열 원소들 중 대각 행렬의 합이다.


1
2
3
>arr = np.array([[1,2,3],[4,5,6],[7,8,9]])
>arr.trace()
15 # 1+5+9


행렬식(determinant)

행렬을 대표하는 값들 중 하나를 말한다. 행렬을 선형 변환을 진행했을 때 얼마나 원벡터가 변하는가에 대한 척도이다. 행렬식을 구하는 방법은 2x2에서 ad - bc에 대한 식이다. 예를 들어 아래와 같은 행렬이 있다고 하자.

\[arr = \left( \begin{matrix} 1 \ 2 \\ 3 \ 4 \end{matrix} \right)\]

이에 대해 행렬 식을 구하면 1x4 - 2x3 = -2가 된다. 이 행렬식이 0이 나온다면 full rank가 아니라는 것을 의미한다. 즉 선형 변환을 하면 차원의 손실이 일어난다.


행렬식을 구하는 함수는 np.linalag.det() 이다.


1
2
3
>arr2 = np.array([[2,3],[1,6]])
>np.linalg.det(arr2)
9.000000000000002


*선형 독립이란?

같은 수의 성분을 가진 n개의 벡터 a1,a2,…,an에 대해 벡터의 1차 결합(linear combination)인 $ C1a1 + C2a2 + … + Cnan = 0 $을 만족하는 상수 C1,C2,…,Cn이 모두 0이면 이 벡터 a1,a2,…,an은 선형 독립에 해당한다. 그러나 하나라도 0이 아닌 Ci가 존재하면 벡터 a1,a2,…,an은 선형종속 또는 1차 종속에 해당한다. 예를 들어 행렬 A,B가 있을 때, $ AC1+BC2 = 0 $에 대해 상수 C1=0,C2=0일때만 만족해야 선형 독립이고, C1=0,C2=0 이외에 C1=1,C2=-1일 때도 0이 된다면 이는 선형 종속이다.

https://rfriend.tistory.com/163


*rank란?

행렬의 일차 독립인 행 또는 열의 최대 개수를 rank라 한다. 임의의 행렬 A의 랭크는 rank(A)로 표기한다. 행렬의 랭크는 그 행렬의 열벡터들에 의해 생성된 벡터 공간의 차원이다. m x n 행렬은 m개의 행과 n개의 열을 가진 행렬이다. 이 행렬의 랭크는 m개의 행 중 일차 독립인 행의 최대 개수 또는 n개의 열 중 일차 독립인 열의 최대 개수와 같다. 모든 행이 서로 일차독립이면 랭크는 m이 될 것이고, 모든 열이 서로 일차독립이면 랭크는 n이 된다. 그렇다면 m x n 행렬에서 모든 행끼리, 모든 열끼리 서로 일차독립이라면 어떻게 될까? 사실 그런 일은 존재하지 않는다. 왜냐하면 최대 일차독립인 행 또는 열의 개수는 한 행렬에서 나온다. 즉, 최대 행 개수와 최대 열 개수는 항상 동일하고, 이는 두 개의 차원이 같다는 것을 의미한다.

\[A = \left( \begin{matrix} 1 \ 2 \ 1 \ 1 \\ 1 \ 1 \ -1 \ 1 \end{matrix} \right)\]

이 행렬 A의 랭크를 구해보자. 1열과 4열은 동일하고, (1,1),(2,1)은 서로 독립이다. (1,1),(2,1),(1,-1)은 서로 종속이다. 왜냐하면 C1=0,C2=0,C3=0 이외에도, C1=3,C2=-2,C3=1일 때도 영벡터가 된다. 따라서 rank(A) = 2이다. 행의 관점에서 보면 1,4열은 이미 같으므로 1행과 2행에서 각각 2,3열만 비교하면 된다. (2,1)과 (1,-1)은 일직선상에 있지 않으므로 독립이다. 따라서 1행과 2행은 독립이다. 또는 어떤 상수 a,b에 대해 $ a(1,2,1,1) + b(1,1,-1,1) = 0 $이 성립하려면 a+b=0, 2a+b=0,a-b=0이어야 하므로 이를 만족하는 유일해는 a = b = 0이므로 독립이다. 따라서 rank(A) = 2이다.

\[B = \left( \begin{matrix} 1 \ 4 \\ -3 \ -12 \end{matrix} \right)\]

행렬 B의 경우 행 관점에서 볼 때 (1,4)를 -3배 하면 2행 (-3,-12)가 얻어지므로 두 벡터는 종속이다. 열 관점에서 봐도 (1,-3)을 4배 하면 (4,-12)가 되므로 역시 종속이므로 rank(B) = 1

종속인지 독립인지를 판단할 때는 벡터를 그림으로 그리거나 정의를 생각하면 이해하기 쉽다. 두 벡터가 동일선상, 즉 같은 직선이 될 때 이 두 벡터가 종속 관계라 한다. 예를 들어 (6,7,8)과 (1,1,1)은 같은 방향에 놓여 있지 않다. 따라서 독립이다.

참고 사이트

  • https://gosamy.tistory.com/16
  • https://rfriend.tistory.com/163


*full rank란?

full rank는 해당 행렬의 행 또는 열 중 작은 값과 rank가 같은 경우를 말한다. 즉 3x2 행렬 A가 있을 때, 이 행렬의 rank(A) = 2라면 이는 full rank라 할 수 있다.


역행렬(inverse matrix)

행렬 A에 대해 AB = BA = I를 만족하는 행렬 B를 구할 수 있다. 즉 B = A^-1을 만족한다.

이를 구하는 함수는 np.linalg.inv()이다.


1
2
3
4
5
6
7
8
9
10
11
12
> arr = np.array([[1,4],[2,3]])
> arr_inv = np.linalg.inv(arr)
> arr_inv
array([[-0.6,  0.8],
       [ 0.4, -0.2]])

> arr.dot(arr_inv)
array([[ 1.00000000e+00,  0.00000000e+00],
       [-1.11022302e-16,  1.00000000e+00]])
# array([[1,0],
#        [0,1]])
# == I


고유값과 고유벡터(eigenvalue and eienvector)

정방행렬(NxN) A에 대해 $Ax = \lambda x$를 만족하는 상수 ⋋와, 이에 대응하는 벡터이다. 즉, $ (A- \lambda I)x = 0 $에 만족해야 하는데, 이를 확인하기 위해 determinant를 사용하여 (A-⋋I)에 대해 0가 되는지 확인하면 고유값을 구할 수 있다.

그러나 numpy에 이를 구하는 함수가 존재한다. 함수는 np.linalg.eig()이다.


1
2
3
4
5
6
> arr = np.array([[2, 0, -2],[1, 1, -2],[0, 0, 1]])
> np.linalg.eig(arr) # output : (⋋, x) 열을 기준으로 1에 대응되는 벡터는 [0,1,0]이다.
(array([1., 2., 1.]), 
 array([[0.        , 0.70710678, 0.89442719],
        [1.        , 0.70710678, 0.        ],
        [0.        , 0.        , 0.4472136 ]]))

고유값이란? 행렬 A를 선형변환으로 봤을 때, 선형 변환 A에 의한 변환 결과가 자기 자신의 상수배가 되는 0이 아닌 벡터를 고유벡터라 하고, 이 상수배 값을 고유값이라 한다.

이 고유값과 고유벡터가 맞는지 확인해보자.

1
2
3
4
5
6
7
> eig_val, eig_vec = np.linalg.eig(arr)

> mat @ eig_vec[:,0] # Ax
array([0., 1., 0.])

> eig_val[0] * eig_vec[:,0] # (lambda)x
array([0., 1., 0.])

동일하게 추출되는 것을 통해 $ Ax = \lambda x $ 를 만족한다는 것을 확인할 수 있다.



Exercise

이 때까지 배운 내용들을 통해 2가지를 직접 생성해보고자 한다.

  1. L2 norm을 구하는 함수 : get_L2_norm()
  2. 어떤 행렬이 singular matrix인지 확인하는 함수 : is_singular()

1. get_L2_norm()

  • 매개변수 : 1차원 벡터, np.array()
  • 반환값 : 인자로 주어진 벡터의 L2 norm 값, number


2. is_singular()

  • 매개변수 : 2차원 벡터, np.array()
  • 반환값 : 인자로 주어진 벡터가 singular이면 true, 아니면 false



Matplotlib

파이썬의 데이터 시각화 라이브러리로 시각화할 때 자주 사용한다.

  • matplotlib 설치
1
pip install matplotlib

쥬피터나 코랩에서 matplotlib을 사용하기 위해서는 %matplotlib inline을 통해 활성화해야 한다.

1
%matplotlib inline


  • Matplotlib 사용해보기
1
2
3
4
5
6
import matplotlib.pyplot as plt

%matplotlib inline

plt.plot([1,2,3,4,5]) # 실제 plotting 하는 함수
plt.show() # plt를 확인하는 명령


  • figure

이 그래프의 크기를 직접 지정해줄 수 있다.

1
2
3
4
plt.figure(figsize=(6,6)) # 6x6 픽셀 크기의 도면을 선언

plt.plot([0,2,3,2,5])
plt.show()


2차함수 그리기

1
2
3
4
5
x = np.array([1,2,3,4,5]) # 정의역
y = np.array([1,4,9,16,25]) # f(x)

plt.plot(x,y)
plt.show()


위의 방법으로는 부드럽게 그려지지 않고 있다. 그래서 더 부드럽게 그리기 위해 전에 배웠던 np.arange를 사용하거나 np.linspace를 통해 점들의 분포를 정의역으로 선언해준다.

1
2
3
4
5
6
7
8
9
x1 = np.arange(-10,10,0.01) # arange(min,max,간격)
plt.plot(x1, x1**2)
plt.title("arange")
plt.show()

x2 = np.linspace(-10,10,2000)
plt.plot(x2, x2**2)
plt.title("linspace")
plt.show()


plot의 추가적인 기능

위에서 사용했던 title도 이에 해당되는 추가적인 기능이다. 그래프가 어떤 것을 의미하는지를 나타내기에 유용하다.

추가적으로 다양한 것들이 있다.

1.x,y 축에 설명 추가

1
2
plt.xlabel("x value")
plt.ylabel("f(x) value")


2.범위 설정

1
plt.axis([-5, 5, 0, 25]) # [x_min, x_max, y_min, y_max]

3.x,y축에 눈금 설정

1
2
plt.xticks([i for i in range(-5,6,1)]) # x축의 눈금 설정, -5,-4,-3...
plt.yticks([i for i in range(0,27,3)]) # y축의 눈금 설정

범위 설정과 눈금설정의 차이는 범위는 그래프의 범위를 설정해서 해당범위만 보겠다는 명령이고, 눈금 설정은 x,y축의 눈금 간격을 직접 세분화하거나 거시화하는 것이다.


4.title

1
plt.title("y = x^2 graph")


5.legend

1
2
plt.plot(x,x**2,label="trend") # label에 이름을 설정만 함
plt.legend() # label을 매핑해서 보여줌, plot이후에 적어줘야 함



Matplotlib의 plot 종류

1. 꺽은선 그래프(plot)

1
2
3
4
5
6
7
8
9
#random.seed(34) # 난수 일정하게 만들기 위한 설정

x = np.arange(0,20) # 0~20
y = np.random.randint(0,20,20) # 난수 20번 생성

plt.axis([0,20,0,20])
plt.yticks([0,5,10,15,20])
plt.plot(x,y)
plt.show()

이 그래프는 시계열 데이터에서 가장 많이 사용한다.


2. 산점도(scatter plot)

1
2
plt.scatter(x,y)
plt.show()

이 그래프는 x와 y가 별개의 변수일 때 많이 사용한다. 별개의 변수 사이에서 어떤 상관관계를 가질지에 대해 파악할 수 있다.


3. 박스 그래프(box plot)

단일 변수가 있을 때 이 변수의 전반적인 분포를 볼 때 많이 사용한다. 특히 수치형 데이터에 대해 많이 사용한다. 이 그래프는 단일 변수도 가능하지만, 2개 이상의 변수도 가능하다.

1
2
plt.boxplot((x,y))
plt.show()

이 그래프는 위 아래로 상한선(max)과 하한선(min)이 존재하고, 박스 그림에서 가장 밑부분, 주황색 선, 가장 윗부분은 각각 Q1(25%),Q2(50%),Q3(75%)에 해당하는 값들이다.


4. 막대 그래프(bar plot)

이 그래프는 범주형 데이터에 많이 사용한다. 범주형 데이터의 값과 그 값의 크기를 직사각형으로 나타낸 그림이다.

1
2
3
plt.bar(x,y)
plt.xticks(np.arange(0,20,1))
plt.show()


5. 히스토그램(histogram)

도수분포를 직사각형의 막대 형태로 나타낸다.0,1,2…이 아니라 0~2까지의 범주형 데이터로 구성 후 그림을 그려준다.

1
2
3
plt.xticks(np.arange(0,20,2))
plt.hist(y,bins=np.arange(0,20,2))
plt.show()

막대 그래프와의 차이점은 박스들이 이어져있다. 그 이유는 히스토그램의 경우 대체로 이어져있는 연속적 데이터를 사용하기 때문이다.


6. 원형 그래프(pie chart)

데이터에서 전체에 대한 부분의 비율을 부채꼴로 나타낸 그래프다. 다른 그래프들과는 다르게 비율을 나타낸다.

1
2
3
4
z = (100,300,200,400)

plt.pie(z, labels=["first","second","third","fourth"])
plt.show()



Seaborn

Matplotlib을 기반으로 더 다양한 시각화 방법을 제공하는 라이브러리다. 다양한 그래프들을 그려볼 수 있다.

  1. 커널 밀도 그래프
  2. 카운트그래프
  3. 캣그래프
  4. 스트립그래프
  5. 히트맵


  • seaborn 설치 및 임포트
1
2
3
!pip install seaborn

import seaborn as sns

seaborn을 통한 시각화 그래프 종류

커널밀도그래프 (Kernel Density plot)

히스토그램과 같은 연속적인 분포를 곡선화해서 그린다.

1
2
3
4
5
6
7
8
9
x = np.arange(0,22,2)
y = np.random.randint(0,20,20)

plt.xticks(np.arange(0,20,2))
plt.hist(y,bins=x)
plt.show()

sns.kdeplot(y)
plt.show()

첫번째 그래프가 히스토그램이고, 두번째가 커널밀도 그래프이다.

이 때, kdeplot에는 shade인자가 있다. 이는 그래프 아래의 영역을 색칠해주는 기능이다.

1
2
sns.kdeplot(y,shade=True)
plt.show()


카운트그래프(count plot)

범주형 column의 빈도수를 시각화하는 기능이다. 이는 groupby 를 한 후의 도수를 하는 것과 동일한 효과를 가진다.

1
2
vote_df = pd.DataFrame({"name":["Andy", "Bob", "Cat"], "vote":[True,True,False]})
vote_df

이를 matplotlib을 통해 생성하려면 groupby를 통해 그려야 했다,.

1
2
3
4
5
6
7
8
9
10
11
12
# in matplotlib
vote_count = vote_df.groupby('vote').count()
vote_count

plt.xlabel("vote")
plt.ylabel("count")
plt.bar(x=[False, True], height=vote_count['name'])
plt.show()

# in sns countplot
sns.countplot(x=vote_df['vote'])
plt.show()

알아서 색상을 지정해서 분류해주고, xlabel,ylabel을 자동으로 지정해준다.


캣 그래프(cat plot)

숫자형 변수와 하나 이상의 범주형 변수의 관계를 보여주는 함수다. 이 그래프는 복잡한 데이터에 적용하는 것이 좋다. cat이 concat, 여러 개를 연결해주는 그래프라는 의미이다.

그래프의 데이터로 사용할 dataset은 다음 사이트에서 다운 받길 바란다. country_wise_lastest.csv : https://www.kaggle.com/code/imdevskp/covid-19-analysis-visualization-comparisons/data?select=country_wise_latest.csv

1
2
3
4
5
6
covid = pd.read_csv("/content/covid_19_Country_Wise_Lastest.csv")
covid.head(5)

s = sns.catplot(x="WHO Region", y="Confirmed", data=covid)
s.fig.set_size_inches(10,6) # 이름들이 겹쳐 나오는 것을 보기 좋게 만들기 위해 figsize
plt.show()

원래는 2차원으로 데이터를 나타냈다면, 추가적으로 hue인자를 통해 각 점의 범주 또는 수치별 관계를 표현할 수도 있다.

또는 kind 인자를 통해 다른 형태의 그래프로도 만들 수 있다. default값은 strip이지만, violin으로 지정해주면 또 다른 형태의 그래프를 그릴 수 있다.


스트립 그래프(strip plot)

위의 캣 그래프의 기본 형태가 스트립 그래프이다.

1
2
sns.stripplot(x="WHO Region", y="Recovered", data=covid)
plt.show()


  • swarmplot

스트립 그래프와 거의 동일한 그래프이다. 동일한 값을 가지는 데이터들이 뭉쳐져 있으면 얼마나 많은 점들이 뭉쳐져 있는지 확인하기 어렵기에 이것들을 양옆으로 분산해준다.

1
2
sns.swarmplot(x="WHO Region", y="Recovered", data=covid)
plt.show()


히트맵(heatmap)

데이터의 행렬을 색상으로 표현해주는 그래프이다.

1
covid.corr() # 행렬의 상관관계를 숫자로 표현

이처럼 숫자로 확인하면 어떤 데이터인지 확인하기 어렵다. 따라서 이를 히트맵으로 생성하면 행렬을 시각화할 수 있다.

1
2
sns.heatmap(covid.corr()) # data
plt.show()


This post is licensed under CC BY 4.0 by the author.

[데브코스] 7주차 - OpenCV corner detection and feature point detection

[데브코스] 9주차 - DeepLearning AI & machine learning