一、python简单实现梯度下降算法
假设这样一个场景:一个人被困在山上,需要从山上下来(找到山的最低点,也就是山谷)。但此时山上的浓雾很大,导致可视度很低;因此,下山的路径就无法确定,必须利用自己周围的信息一步一步地找到下山的路。这个时候,便可利用梯度下降算法来帮助自己下山。怎么做呢,首先以他当前的所处的位置为基准,寻找这个位置最陡峭的地方,然后朝着下降方向走一步,然后又继续以当前位置为基准,再找最陡峭的地方,再走直到最后到达最低处;同理上山也是如此,只是这时候就变成梯度上升算法了。
梯度下降法的基本思想就可以类比为一个下山的过程。首先将特征值映射到实际值从而得到我们的预测函数为
其次不断迭代更新该函数的m和c,从而最小化该函数的目标函数,就像沿着山的最陡的方向走下山,到达谷底。
代码如下,我们先假设以及,从而建立了一个的函数,其中x的取值范围为[-3,3]。
import numpy as npimport matplotlib.pyplot as pltfrom IPython import displayimport time%matplotlib inlinenp.random.seed(seed=1001)x = np.linspace(-2,2, 4)m_true = 1.4c_true = -3.1y = m_true*x+c_trueplt.plot(x, y, 'r.', markersize=10) # plot data as red dotsplt.xlim([-3, 3])
从而得到
但由于这些点都在一条直线上,这是非常不符合实际情况的,所以我们需要用一些高斯噪声去破坏他们,从而得到下图。
np.random.seed(seed=22050)noise = np.random.normal(scale=0.5, size=4) # standard deviation of the noise is 0.5y = m_true*x + c_true + noiseplt.plot(x, y, 'r.', markersize=10)plt.xlim([-3, 3])
之后我们建立了100*100的网格去可视化误差函数,并可以绘出误差值等高线图。其中等高线代表着误差值,而横纵坐标分别表示这着网格中m和c的值。
# create an array of linearly separated values around m_truem_vals = np.linspace(m_true-3, m_true+3, 100)# create an array of linearly separated values aec_vals = np.linspace(c_true-3, c_true+3, 100) m_grid, c_grid = np.meshgrid(m_vals, c_vals) E_grid = np.zeros((100, 100))for i in range(100): for j in range(100): E_grid[i, j] = ((y - m_grid[i, j]*x - c_grid[i, j])**2).sum() def regression_contour(f, ax, m_vals, c_vals, E_grid): "Regression contour plot." hcont = ax.contour(m_vals, c_vals, E_grid, levels=[0, 0.25, 0.5, 1, 2, 4, 8, 16, 32, 64]) # this makes the contour plot plt.clabel(hcont, inline=1, fontsize=10) # this labels the contours. ax.set_xlabel('$m$', fontsize=20) ax.set_ylabel('$c$', fontsize=20) f, ax = plt.subplots(figsize=(5,5))regression_contour(f, ax, m_vals, c_vals, E_grid)
再根据目标函数公式
可以得出偏移梯度
及坡度
随后我们将不断迭代更新优化偏移梯度以及坡度以实现最小化误差值。
def plot_regression_contour_fit(f, ax, x, y, learn_rate = 0.1, iters = 10): m_star = 0.0 c_star = -5.0 E = np.empty(iters+1) E[0] = ((y - m_star*x - c_star)**2).sum() regression_contour(f, ax, m_vals, c_vals, E_grid) ax.plot(m_star, c_star, 'g*', markersize=10) plt.pause(1.5) c_vec = c_star m_vec = m_star for i in range(iters): c_grad = -2*(y-m_star*x - c_star).sum() m_grad = -2*(x*(y-m_star*x - c_star)).sum() #c_ant = c_star #m_ant = m_star c_star = c_star - learn_rate*c_grad m_star = m_star - learn_rate*m_grad c_vec = np.append(c_vec, c_star) m_vec = np.append(m_vec, m_star) display.clear_output(wait=True) f, ax = plt.subplots(figsize=(7,7)) regression_contour(f, ax, m_vals, c_vals, E_grid) ax.plot(m_vec[0:m_vec.shape[0]-1], c_vec[0:c_vec.shape[0]-1], 'r*', markersize=10) ax.plot(m_star, c_star, 'g*', markersize=10) ax.plot(m_vec, c_vec, 'k') E[i+1] = ((y - m_star*x - c_star)**2).sum() print("Iteration {} Objective function: {:02.4f}.".format(i+1, E[i+1])) plt.pause(1.2) return m_star, c_starfax = plt.subplots(figsize=(5,5), dpi=80)m_star, c_star = plot_regression_contour_fit(f, ax, x, y, learn_rate = 0.05, iters=5)
在代码中,我们迭代更新了m和c5次。最后可以看出误差值在网格中的位置不断由等高线高的位置朝等高线低的位置移动,即由海拔高的地方向海拔低的地方移动。误差值也由最初的4.6438下降到了1.1633。
二、梯度下降算法应用——电影推荐系统
数据源如下图所示,数据源中包含了userId(用户id),movieId(电影id),rating(评分)和tag(标签)。
之后,由于用户数量较多,我们定义了用户样本数量'nUsersInexample'为10,并随机选择'nUsers'和他们对电影的评分,最后利用'my_batch_users'列表创建矩阵Y。矩阵Y中包含了用户id,电影id,原始评分以及原始评分减去平均值的评分,从而在之后的坐标系中,观众对电影的评分可以均匀地分布在坐标系零点的两侧。
nUsersInExample = 10 # The maximum number of Users we're going to analyse at one timeindexes_unique_users = ratings['userId'].unique()n_users = indexes_unique_users.shape[0]indexes_users = np.random.permutation(n_users)my_batch_users = indexes_users[0:nUsersInExample]# We need to make a list of the movies that these users have watchedlist_movies_each_user = [[] for _ in range(nUsersInExample)]list_ratings_each_user = [[] for _ in range(nUsersInExample)]list_movies = []list_ratings = []list_users = []for i in range(1, nUsersInExample): # Movies local_list_per_user_movies = ratings['movieId'][ratings['userId'] == my_batch_users[i]].values list_movies_each_user[i] = local_list_per_user_movies list_movies = np.append(list_movies,local_list_per_user_movies) # Ratings local_list_per_user_ratings = ratings['rating'][ratings['userId'] == my_batch_users[i]].values list_ratings_each_user[i] = local_list_per_user_ratings list_ratings = np.append(list_ratings, local_list_per_user_ratings) # Users n_each_user = local_list_per_user_movies.shape[0] local_rep_user = my_batch_users[i]*np.ones((1, n_each_user)) list_users = np.append(list_users, local_rep_user) # Let us first see how many unique movies have been ratedindexes_unique_movies = np.unique(list_movies)n_movies = indexes_unique_movies.shape[0]p_list_ratings = np.concatenate(list_ratings_each_user).ravel()p_list_ratings_original = p_list_ratings.tolist()mean_ratings_train = np.mean(p_list_ratings)p_list_ratings = p_list_ratings - mean_ratings_train # remove the meanp_list_movies = np.concatenate(list_movies_each_user).ravel().tolist()p_list_users = list_users.tolist()Y = pd.DataFrame({'users': p_list_users, 'movies': p_list_movies, 'ratingsorig': p_list_ratings_original,'ratings':p_list_ratings.tolist()})
推荐系统旨在根据用户的喜好对商品(电影、书籍及其他商业产品)提出建议。推荐引擎需要考虑到所有用户的口味和每个对象的特性。而组织对象常见的一种方法为将具有相近特征的对象放置在空间的相近位置。所以在电影推荐系统中,用户以及电影名称以散点的形式展现在图像上,并基于梯度下降算法不断优化用户以及电影之间的距离,从而对特定用户推荐电影——相邻特定用户位置最近的电影。
首先,我们需要为每个用户和电影定义他们在坐标系中的位置。对于一个2D平面,我们需要将用户和电影的位置分别存在两个数字向量中。即
接下来,我们需要一个度量来确定电影和用户之间的相似性,即向量的内积。向量的内积指的是向量中每个元素的乘积的总和,即
电影推荐系统可通过该公式计算电影和用户之间的相似性。用户和电影的相似越高,用户给电影的评分就越高,正好对应了数据中所包含的评分信息。所以我们现需要建立两个包含用户坐标和电影坐标的大向量集,也就是特征向量集
和
从而有目标函数
其中是一个布尔变量,1则是用户为此电影打过分,0则是用户未给此电影打过分。之后可通过梯度下降算法
优化目标函数。而
同理可得
在优化目标函数的过程中,我们首先需要初始化矩阵U和矩阵V。这可以通过pandas中DataFrame利用随机较小的值初始它们的位置。
q = 2 # the dimension of our map of the 'library'learn_rate = 0.01U = pd.DataFrame(np.random.normal(size=(nUsersInExample, q))*0.001, index=my_batch_users)V = pd.DataFrame(np.random.normal(size=(n_movies, q))*0.001, index=indexes_unique_movies)
所以U和V可以说是两个随机的数据集,为每个用户和电影随机了一个位置出来,让他们不断优化以寻找自己在坐标系中的位置。优化目标函数代码如下,即梯度下降算法
def objective_gradient(Y, U, V): gU = pd.DataFrame(np.zeros((U.shape)), index=U.index) gV = pd.DataFrame(np.zeros((V.shape)), index=V.index) obj = 0. nrows = Y.shape[0] for i in range(nrows): row = Y.iloc[i] user = row['users'] film = row['movies'] rating = row['ratings'] prediction = np.dot(U.loc[user], V.loc[film]) # vTu diff = prediction - rating # vTu - y obj += diff*diff gU.loc[user] += 2*diff*V.loc[film] gV.loc[film] += 2*diff*U.loc[user] return obj, gUgViterations = 20for i in range(iterations): obj, gU, gV = objective_gradient(Y, U, V) print("Iteration", i, "Objective function: ", obj) U -= learn_rate*gU V -= learn_rate*gV
可以看出,误差值随着迭代次数增加而逐渐下降。然而将U和V两个坐标集向量全部迭代一遍,即传统的梯度下降算法,运行所花的时间较长且误差值下降速率较慢。所以随机梯度下降算法应运而生,通过U和V两个坐标集中的随机点迭代,可以看到误差值明显的下降
def stochastic_descent(obj, Y, U, V, learn_rate): Y = Y.iloc[np.random.permutation(len(Y))] obj = 0. nrows = Y.shape[0] for i in range(nrows): row = Y.iloc[i] user = row['users'] film = row['movies'] rating = row['ratings'] prediction = np.dot(U.loc[user], V.loc[film]) diff = prediction - rating obj += diff**2 U.loc[user] -= 2*learn_rate*diff*V.loc[film] V.loc[film] -= 2*learn_rate*diff*U.loc[user] return obj, U, Vlearn_rate = 0.05iterations = 20obj = 0.for i in range(iterations): obj, U, V = stochastic_descent(obj, Y, U, V, learn_rate) print("update {} objective function: {}".format(i+1, obj))
而从最后显示的2D用户和电影的坐标散点图可以看到电影坐标的散点已经分散,以寻找相性较大的用户坐标。
随着迭代次数的增加,坐标图中电影的散点会变得更加分散,并与相性较大的用户的距离变得更近,从而给出用户坐标临近的电影坐标作为电影推荐给用户。