3월달 "강화학습의 이론과 실제" 로 강의했던 강의자료 배포합니다.
1.Dynamic Programming
2.Policy iteration
3.Value iteration
4.Monte Carlo method
5.Temporal-Difference Learning
6.Sarsa
7.Q-learning
8.딥러닝 프레임워크 케라스 소개 및 슈퍼마리오 환경 구축
9.DQN을 이용한 인공지능 슈퍼마리오 만들기
이 흐름으로 강의를 했는데
브레이크아웃 설명은 양혁렬 (Hyuk Ryeol Yang)님의 코드를 참고 하였고
8번,9번은 새로운 환경이 나왔으니 무시해도 좋겠습니다.
이 환경에 대한 자료는 주말까지 작성하고 업로드 할 예정입니다.
3월달 "강화학습의 이론과 실제" 로 강의했던 강의자료 배포합니다.
1.Dynamic Programming
2.Policy iteration
3.Value iteration
4.Monte Carlo method
5.Temporal-Difference Learning
6.Sarsa
7.Q-learning
8.딥러닝 프레임워크 케라스 소개 및 슈퍼마리오 환경 구축
9.DQN을 이용한 인공지능 슈퍼마리오 만들기
이 흐름으로 강의를 했는데
브레이크아웃 설명은 양혁렬 (Hyuk Ryeol Yang)님의 코드를 참고 하였고
8번,9번은 새로운 환경이 나왔으니 무시해도 좋겠습니다.
이 환경에 대한 자료는 주말까지 작성하고 업로드 할 예정입니다.
Session 4 - 최효석 React Hooks 마법. 그리고 깔끔한 사용기
2019년 9월 6일 네이버 쇼핑 개발자 meet up 행사인 'SHOWROOM' 에 발표된 자료입니다.
보다 자세한 내용은 http://nshop-developer.github.io 을 참고해주세요.
(2019년 9월 30일 오후 오픈 예정)
Coursera Machine Learning (by Andrew Ng)_강의정리SANG WON PARK
단순히 공식으로 설명하지 않고, 실제 코드 및 샘플데이터를 이용하여 수식의 결과가 어떻게 적용되는지 자세하게 설명하고 있다.
처음 week1 ~ week4 까지는 김성훈 교수님의 "모두를 위한 딥러닝"에서 한번 이해했던 내용이라 좀 쉽게 진행했고, 나머지는 기초가 부족한 상황이라 다른 자료를 꽤 많이 참고하면서 학습해야 했다.
여러 도서나 강의를 이용하여 머신러닝을 학습하려고 했었는데, 이 강의만큼 나에게 맞는것은 없었던거 같다. 특히 Octave code를 이용한 실습자료는 나중에도 언제든 활용가능할 것 같다.
Week1
Linear Regression with One Variable
Linear Algebra - review
Week2
Linear Regression with Multiple Variables
Octave[incomplete]
Week3
Logistic Regression
Regularization
Week4
Neural Networks - Representation
Week5
Neural Networks - Learning
Week6
Advice for applying machine learning techniques
Machine Learning System Design
Week7
Support Vector Machines
Week8
Unsupervised Learning(Clustering)
Dimensionality Reduction
Week9
Anomaly Detection
Recommender Systems
Week10
Large Scale Machine Learning
Week11
Application Example - Photo OCR
모두를 위한 Deep Reinforcement Learning 강의를 요약정리
http://hunkim.github.io/ml/
실습에 사용된 코드
https://github.com/freepsw/tensorflow_examples/tree/master/20.RL_by_SungKim
[함수형 사고] 책을 읽고 진행한 PT
Java 8, Scala, Clojure, Groovy 등의 함수형 프로그래밍 언어에서 사용하고 있는 패러다임을 익힐 수 있습니다.
책을 요약하고 PT를 진행하려고 하니 미숙한 부분이 많으니, 자세한 부분은 책을 참고 부탁드립니다.
책에서는 주로 Groovy에 대한 코드가 많았는데, Scala에 조금 더 익숙하기 때문에 Scala로 작성한 코드가 많습니다.
리덕스를 도입할 때 주저하게 만드는 장벽들
○ 개요
몹엑스, 아폴로, 컨텍스트 API 등 리덕스를 도입하기도 전에 이미 선택적인 고민을 하게 만듭니다. 리덕스를 도입한 후에도 사가, 펜더, 옵져버블등의 미들웨어의 도입에서도 선택 장애가 발생하는 문제들이 리덕스 선택을 주저하게 만듭니다.
2020년 리덕스는 좋은 선택지 일까요? 리덕스는 언제 그리고 어떻게 사용해야 활용도를 높일 수 있을까요? 에어비앤비 결제 시스템과 3차원 시각화 시스템에 적용한 리덕스 활용 사례를 살펴보며 리덕스의 효용 가치를 같이 살펴보고자 합니다.
○ 목차
- 형상관리를 어렵게 하는 요소들
- MobX vs Redux
- 단방향 데이터 흐름(Flux)
- 미들웨어 살펴보기
- 리덕스와 미들웨어 활용 데모
- Typescript와 GraphQL 도입 사례
Session 4 - 최효석 React Hooks 마법. 그리고 깔끔한 사용기
2019년 9월 6일 네이버 쇼핑 개발자 meet up 행사인 'SHOWROOM' 에 발표된 자료입니다.
보다 자세한 내용은 http://nshop-developer.github.io 을 참고해주세요.
(2019년 9월 30일 오후 오픈 예정)
Coursera Machine Learning (by Andrew Ng)_강의정리SANG WON PARK
단순히 공식으로 설명하지 않고, 실제 코드 및 샘플데이터를 이용하여 수식의 결과가 어떻게 적용되는지 자세하게 설명하고 있다.
처음 week1 ~ week4 까지는 김성훈 교수님의 "모두를 위한 딥러닝"에서 한번 이해했던 내용이라 좀 쉽게 진행했고, 나머지는 기초가 부족한 상황이라 다른 자료를 꽤 많이 참고하면서 학습해야 했다.
여러 도서나 강의를 이용하여 머신러닝을 학습하려고 했었는데, 이 강의만큼 나에게 맞는것은 없었던거 같다. 특히 Octave code를 이용한 실습자료는 나중에도 언제든 활용가능할 것 같다.
Week1
Linear Regression with One Variable
Linear Algebra - review
Week2
Linear Regression with Multiple Variables
Octave[incomplete]
Week3
Logistic Regression
Regularization
Week4
Neural Networks - Representation
Week5
Neural Networks - Learning
Week6
Advice for applying machine learning techniques
Machine Learning System Design
Week7
Support Vector Machines
Week8
Unsupervised Learning(Clustering)
Dimensionality Reduction
Week9
Anomaly Detection
Recommender Systems
Week10
Large Scale Machine Learning
Week11
Application Example - Photo OCR
모두를 위한 Deep Reinforcement Learning 강의를 요약정리
http://hunkim.github.io/ml/
실습에 사용된 코드
https://github.com/freepsw/tensorflow_examples/tree/master/20.RL_by_SungKim
[함수형 사고] 책을 읽고 진행한 PT
Java 8, Scala, Clojure, Groovy 등의 함수형 프로그래밍 언어에서 사용하고 있는 패러다임을 익힐 수 있습니다.
책을 요약하고 PT를 진행하려고 하니 미숙한 부분이 많으니, 자세한 부분은 책을 참고 부탁드립니다.
책에서는 주로 Groovy에 대한 코드가 많았는데, Scala에 조금 더 익숙하기 때문에 Scala로 작성한 코드가 많습니다.
리덕스를 도입할 때 주저하게 만드는 장벽들
○ 개요
몹엑스, 아폴로, 컨텍스트 API 등 리덕스를 도입하기도 전에 이미 선택적인 고민을 하게 만듭니다. 리덕스를 도입한 후에도 사가, 펜더, 옵져버블등의 미들웨어의 도입에서도 선택 장애가 발생하는 문제들이 리덕스 선택을 주저하게 만듭니다.
2020년 리덕스는 좋은 선택지 일까요? 리덕스는 언제 그리고 어떻게 사용해야 활용도를 높일 수 있을까요? 에어비앤비 결제 시스템과 3차원 시각화 시스템에 적용한 리덕스 활용 사례를 살펴보며 리덕스의 효용 가치를 같이 살펴보고자 합니다.
○ 목차
- 형상관리를 어렵게 하는 요소들
- MobX vs Redux
- 단방향 데이터 흐름(Flux)
- 미들웨어 살펴보기
- 리덕스와 미들웨어 활용 데모
- Typescript와 GraphQL 도입 사례
1. Policy Gradient
Policy gradient는 DQN처럼 value function을 근사한 다음 주어진 상태의 value function 값에 따라 어떤 행동을
취할지 결정하는, 즉 implict poilcy를 가지는 다른 강화학습 알고리즘들과 달리 policy를 직접 함수로 나타낸 뒤 학
습하는 강화학습 알고리즘이다.
Policy Gradient 논문 을 따라가면서 이런 방식의 접근이 어떤 점에서 어려운지, 어떻게 해결했는지 알아보고
CartPole에 대해 구현한 코드를 살펴보자.
Policy Gradient를 적용하기 위한 제약조건은 단순하다. Policy를 근사하는 함수를 라고 할 때, 는 미분 가능해야
한다. Gradient 값을 근사해서 policy를 학습하는 것이 주 요지이므로 이는 당연한 제약이라 할 수 있다. Neural
Network 또한 미분 가능하기 때문에 Policy Gradient의 function approximator로 사용할 수 있다.
Policy function 는 상태 s, 행동 a, 매개변수 를 받아 상태 s에서 행동 a를 할 확률을 돌려주는 함수이다. 즉
이다. 실제로 어떤 행동을 할 지는 이 확률에 따라서 stochastic하게 고른다.
이제 우리는 policy가 얼마나 좋은지 측정할 수 있는 함수, 즉 loss function을 정의한 뒤 이를 통해 policy를 업데이
트 할 것이다. loss function에는 두가지 형태가 있는데, 여기서는 출발 상태가 로 고정 됐을 때의 loss function인
로 예시를 들겠다. 로 정의하면
가 성립한다. 직관적인 의미를 생각해보면 는 시간에 따른 비율을 고려한 보
상의 기대값의 합이고, 는 policy를 따라 행동했을 때 어떤 상태 s에서 머무르고 있을 확률을 뜻한다.
이제 해야 할 일은 값에 따라 를 gradient descent로 업데이트 해주면 된다. 하지만, 우리는 무한히 시뮬레이
션을 해 볼 수 없기 때문에 도 계산할 수 없고, 각 (상태, 행동) 쌍의 가치를 나타내는 도 알지 못한다. 이것을 알
고 있다면 그냥 Q-learning policy를 사용하면 될 일이다. 그러면 이걸 어떻게 계산해야 할까?
REINFORCE: Monte Carlo Policy Gradient
REINFORCE는 몬테카를로를 통해 위의 gradient 값을 근사하는 알고리즘 이름으로, Policy gradient 논문 이전에
발표된 것이다. 미분값이 , 즉 policy의 state distribution을 따르므로
라고 볼 수 있다. 여기서 expectation은 state, 즉 에 대해 취해진 것이다. 마찬가지로, 분자와 분모에 를 곱
해주면 가 되는 것을 알 수 있다.
그런데, 우리가 실제로 한번의 시뮬레이션을 통해 얻은 reward 합이 라고 하면, 함수의 정의에 의해
가 성립한다. 즉 로 고쳐 쓸 수 있고, 는 모두 우리가 직접 계산할 수
있는 값이기 때문에 충분한 샘플링을 거쳐 값을 근사할 수 있게 된다. 즉 다음 업데이트를 반복해주면 policy가
점점 좋아진다.
그런데 이렇게 업데이트를 진행할 경우, 이론상에선 문제가 없지만 실제 구현할 때에는 행동을 한 번 할 때마다 미
분값과 그 때 forward propagation 값을 저장해두어야 한다는 단점이 있다. 따라서 인 점을 활용해
loss function을 로 두면 기존 tensorflow나 pytorch 등의 라이브러리로 쉽게 구현할 수 있다.
2. 알고리즘을 정리하면 다음과 같다.
구현 (tensorflow)
텐서플로우를 사용해 policy gradient를 CartPole에 대해 구현한 코드를 살펴보자.
pg라는 클래스를 만들어 CartPole 환경과 텐서플로우 세션, 그리고 policy를 나타낼 neural network를 만들어준
다.
pg의 멤버 변수를 모두 세팅한 후, 실험은 위와 같은 코드로 진행할 것이다.
class pg:
def __init__(self):
self.env = gym.make('CartPole-v0')
self.sess = tf.Session()
self.net = self.build_policy()
self.sess.run(tf.global_variables_initializer())
def main():
agent = pg()
agent.train()
if __name__ == '__main__':
main()
'''
CartPole의 observation을 state로 받아서
각 행동의 unnormalized prob.을 내보내는 네트워크.
[N, 4] -> [N, 2]
reward : Sutton book의 REINFORCE 중 각 time step 별 G를 모은 리스트. reward[t] == time step t
에서의 G
action : 각 time step 별 선택됐던 action은 1, 아니면 0
'''
def build_policy(self):
state = tf.placeholder(tf.float32, shape=(None, 4))
3. 위의 코드는 실제 policy를 나타낼 neural network를 구성하는 코드다. 각 라인별로 살펴보면,
한 에피소드 동안 쌓인 상태/보상/취했던 행동을 입력으로 받아서
64개의 유닛을 가진 히든레이어 하나 짜리 DNN을 만들고, 출력을 softmax 한 값을 각 행동을 취할 확률로 삼는다.
reward = tf.placeholder(tf.float32, shape=(None, 1))
action = tf.placeholder(tf.float32, shape=(None, 2))
layer = tf.layers.dense(state, 64, activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(L2_REG))
logit = tf.layers.dense(layer, 2, activation=None,
kernel_regularizer=tf.contrib.layers.l2_regularizer(L2_REG))
log_prob = tf.nn.log_softmax(logit)
prob = tf.nn.softmax(logit)
loss = tf.multiply(log_prob, action)
loss = tf.reduce_sum(loss, axis=1, keepdims=True)
loss = tf.multiply(loss, reward)
loss = -tf.reduce_sum(loss) +
tf.reduce_sum(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES))
optimize = tf.train.AdamOptimizer(LR).minimize(loss) # 1e-2 / 1e-4
return {
'optimize': optimize, 'loss': loss, 'log_prob': log_prob,
'state': state, 'reward': reward, 'action': action,
'prob': prob
}
state = tf.placeholder(tf.float32, shape=(None, 4))
reward = tf.placeholder(tf.float32, shape=(None, 1))
action = tf.placeholder(tf.float32, shape=(None, 2))
layer = tf.layers.dense(state, 64, activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(L2_REG))
logit = tf.layers.dense(layer, 2, activation=None,
kernel_regularizer=tf.contrib.layers.l2_regularizer(L2_REG))
log_prob = tf.nn.log_softmax(logit)
prob = tf.nn.softmax(logit)
loss = tf.multiply(log_prob, action)
loss = tf.reduce_sum(loss, axis=1, keepdims=True)
loss = tf.multiply(loss, reward)
loss = -tf.reduce_sum(loss) +
tf.reduce_sum(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES))
4. 위의 식에 따라 loss를 계산한다. 이 때 주의할 점은 우리가 정의한 는 최소화의 대상이 아니라 최대화의 대상이기
때문에, tf.reduce_sum(loss)에 -1을 곱해줘야 한다.
이제 optimizer를 설정해주고 각 오퍼레이터를 사용할 수 있게 리턴해준다.
이제 실제 학습을 어떻게 하는 지 알아보자.
optimize = tf.train.AdamOptimizer(LR).minimize(loss) # 1e-2 / 1e-4
return {
'optimize': optimize, 'loss': loss, 'log_prob': log_prob,
'state': state, 'reward': reward, 'action': action,
'prob': prob
}
def train(self):
gamma = 0.9
history = []
for episode in range(1, 10000 + 1):
#render = True if episode % 100 == 1 else False
render = False
obs, done, state, reward, action, G = self.env.reset(), False, [], [], [], []
while not done:
if render:
self.env.render()
prob = self.sess.run(self.net['prob'], feed_dict={
self.net['state']: np.array(obs).reshape((-1, 4))
})[0]
act = np.random.choice(list(range(2)), p=prob)
state.append(obs)
obs, rew, done, _ = self.env.step(act)
rew = 0.0 if abs(rew) < 1e-5 else (1.0 if rew > 0 else -1.0)
action.append(act)
reward.append(rew)
for t in range(len(reward)):
reward[t] = reward[t] * pow(gamma, t)
for t in range(len(reward)):
G.append(np.sum(reward[t:]))
state = np.reshape(np.array(state), (-1, 4))
reward = np.reshape(np.array(G), (-1, 1))
reward = (reward - np.mean(reward)) / (np.std(reward) + 1e-5)
action = np.eye(2)[np.array(action).reshape(-1)] # one-hot
action = np.reshape(np.array(action), (-1, 2))
self.sess.run(self.net['optimize'], feed_dict={
self.net['state']: state, self.net['reward']: reward, self.net['action']: action
})
history.append(reward.shape[0])
if episode % 100 == 0:
5. 총 10000번의 에피소드(한 번 죽거나 성공하는게 한 에피소드)동안 학습을 진행한다.
각 에피소드가 시작될 때마다 초기화를 해주고,
현재 상태에 대한 확률 계산 -> 확률에 따른 행동 선택 -> 행동 수행 -> 결과 관찰 을 반복한다.
그리고 저장한 reward 값에 discount factor를 적용시켜준다. 즉 시간에 따른 감가상각을 반영해준다.
이제 python list로 저장된 상태 / 보상 / 행동을 numpy 로 가공한 뒤, optimize에 인자로 넘겨줘 policy를 업데이트
한다.
구현 (pytorch)
print('%dth try : %d step, avg %s' % (episode, reward.shape[0],
np.mean(history[-200:])))
render = False
obs, done, state, reward, action, G = self.env.reset(), False, [], [], [], []
while not done:
if render:
self.env.render()
prob = self.sess.run(self.net['prob'], feed_dict={
self.net['state']: np.array(obs).reshape((-1, 4))
})[0]
act = np.random.choice(list(range(2)), p=prob)
state.append(obs)
obs, rew, done, _ = self.env.step(act)
rew = 0.0 if abs(rew) < 1e-5 else (1.0 if rew > 0 else -1.0)
action.append(act)
reward.append(rew)
for t in range(len(reward)):
reward[t] = reward[t] * pow(gamma, t)
for t in range(len(reward)):
G.append(np.sum(reward[t:]))
state = np.reshape(np.array(state), (-1, 4))
reward = np.reshape(np.array(G), (-1, 1))
reward = (reward - np.mean(reward)) / (np.std(reward) + 1e-5)
action = np.eye(2)[np.array(action).reshape(-1)] # one-hot
action = np.reshape(np.array(action), (-1, 2))
self.sess.run(self.net['optimize'], feed_dict={
self.net['state']: state, self.net['reward']: reward, self.net['action']: action
})
6. pytorch로 구현한 버전도 살펴보자.
import random
import gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical
L2_REG = 1e-2
LR = 1e-2
'''
CartPole의 observation을 state로 받아서
각 행동의 unnormalized prob.을 내보내는 네트워크.
[N, 4] -> [N, 2]
reward : Sutton book의 REINFORCE 중 각 time step 별 G를 모은 리스트. reward[t] == time step t에서
의 G
action : 각 time step 별 선택됐던 action은 1, 아니면 0
'''
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(4, 32)
self.fc2 = nn.Linear(32, 2)
self.history = []
def forward(self, x):
state, reward, action = x
n = state.size()[0]
layer = F.relu(self.fc1(state))
logit = self.fc2(layer)
log_prob = F.log_softmax(logit, dim=1)
loss = log_prob * action
loss = torch.sum(loss, 1, True)
loss = loss * reward
loss = torch.sum(loss)
return -loss
def predict(self, x):
with torch.no_grad():
x = F.relu(self.fc1(x))
x = self.fc2(x)
res = F.softmax(x, dim=1)
return res
#return log_prob
class pg:
7. def __init__(self):
self.env = gym.make('CartPole-v0')
self.net = Net()
self.optimizer = optim.RMSprop(self.net.parameters(), lr=LR, weight_decay=L2_REG)
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(self.optimizer, mode='max')
def train(self):
gamma = 0.99
history = []
for episode in range(1, 10000 + 1):
render = True if episode % 100 == 0 and episode else False
obs, done, state, reward, action, G = self.env.reset(), False, [], [], [], []
while not done:
if render:
self.env.render()
prob = self.net.predict(torch.from_numpy(np.array(obs).reshape((-1, 4))
).float()).detach().numpy()[0]
act = np.random.choice(list(range(2)), p=prob)
state.append(obs)
obs, rew, done, _ = self.env.step(act)
action.append(act)
reward.append(rew)
G = [0.0 for _ in range(len(reward))]
Gr = 0.0
for i in range(len(reward)-1, -1, -1):
G[i] = Gr = reward[i] + gamma * Gr
state = np.reshape(np.array(state), (-1, 4))
reward = np.reshape(np.array(G), (-1, 1))
reward = (reward - np.mean(reward)) / (np.std(reward) + 1e-5)
action = np.eye(2)[np.array(action).reshape(-1)] # one-hot
action = np.reshape(np.array(action), (-1, 2))
state = torch.from_numpy(state).float()
reward = torch.from_numpy(reward).float()
action = torch.from_numpy(action).float()
self.optimizer.zero_grad()
loss = self.net([state, reward, action])
loss.backward()
self.optimizer.step()
if episode and episode % 50 == 0:
self.scheduler.step(np.mean(history[-200:]))
history.append(reward.shape[0])
if episode % 50 == 0:
print('%dth try : %d step, avg %s, lr %s' % (episode, reward.shape[0],
np.mean(history[-200:]), self.optimizer.param_groups[0]['lr']))
def main():
8. 텐서플로우 버전과 구조는 거의 비슷하다. 다른 점은
pytorch는 learning rate scheduling이 간편하기 때문에 적용했다는 점과,
optimize를 operator로 빼는 것이 아닌 gradient flush -> back propagation -> apply optimizer를 명시적으로 해
준다는 점이다. Learning rate scheduler의 mode가 max인 이유는 metric, 즉 평균 에피소드 길이를 최대화 하는
방향으로 학습해야하기 때문이다.
실제로 pytorch 버전을 실행시키면
900 에피소드 만에 학습이 완료되는 것을 볼 수 있다.
https://imgur.com/a/bkdaytw 에서 실제 학습 결과를 볼 수 있다.
agent = pg()
agent.train()
if __name__ == '__main__':
main()
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(self.optimizer, mode='max')
self.optimizer.zero_grad()
loss = self.net([state, reward, action])
loss.backward()
self.optimizer.step()
$ python torch_pg.py
?[33mWARN: gym.spaces.Box autodetected dtype as <class 'numpy.float32'>. Please provide explicit
dtype.?[0m
50th try : 200 step, avg 97.68, lr 0.01
100th try : 200 step, avg 137.47, lr 0.01
150th try : 200 step, avg 149.85333333333332, lr 0.01
200th try : 200 step, avg 161.53, lr 0.01
250th try : 200 step, avg 184.48, lr 0.01
300th try : 200 step, avg 188.565, lr 0.01
350th try : 178 step, avg 176.955, lr 0.01
400th try : 143 step, avg 172.125, lr 0.01
450th try : 200 step, avg 171.25, lr 0.01
500th try : 200 step, avg 171.86, lr 0.01
550th try : 200 step, avg 189.815, lr 0.01
600th try : 200 step, avg 183.51, lr 0.01
650th try : 200 step, avg 187.015, lr 0.01
700th try : 200 step, avg 177.83, lr 0.01
750th try : 200 step, avg 177.83, lr 0.01
800th try : 200 step, avg 189.825, lr 0.01
850th try : 200 step, avg 189.825, lr 0.01
900th try : 200 step, avg 200.0, lr 0.01
9. 참고문헌
[1] Richard S. Sutton, David McAllester, Satinder Singh, Yishay Mansou. Policy Gradient Methods for
Reinforcement Learning with Function Approximation.
[2] Sutton, R. S., Barto, A. G. Reinforcement Learning: An Introduction. 2nd edition draft.