日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

全连接层的作用_python构建计算图2——全连接层

發(fā)布時(shí)間:2025/3/12 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 全连接层的作用_python构建计算图2——全连接层 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

(好久不更~)前文中,參照tensorflow的方式實(shí)現(xiàn)了簡(jiǎn)單的自動(dòng)求導(dǎo)。接下來(lái)要在自動(dòng)求導(dǎo)的基底(模板)上搭建簡(jiǎn)單的bp神經(jīng)網(wǎng)絡(luò)。

計(jì)算圖

前文曾多次提到計(jì)算圖,關(guān)于什么是計(jì)算圖,有很多種說法。既然它被稱為圖,便具有圖的基本元素:點(diǎn)和線。如下圖:

點(diǎn):節(jié)點(diǎn),用來(lái)儲(chǔ)存變量。比如輸入X,隱含層h,輸出y

線(箭頭):操作(算符),用來(lái)確定兩個(gè)節(jié)點(diǎn)之間的聯(lián)系,或者說由前一個(gè)節(jié)點(diǎn)經(jīng)過這個(gè)操作后可以得到后面的節(jié)點(diǎn)。

可以利用上一篇構(gòu)造復(fù)合函數(shù)的方式來(lái)理解它。構(gòu)造函數(shù)f(x),需要自變量x和一個(gè)映射f,這里的x是節(jié)點(diǎn),映射f是操作。如果另y=f(x),那么y就是新的節(jié)點(diǎn),映射f的箭頭是從x指向y。多個(gè)不同的映射可以組成一個(gè)大網(wǎng)絡(luò),儲(chǔ)存更復(fù)雜的信息。

計(jì)算圖的好處在于,它是微分鏈?zhǔn)椒▌t的直觀體現(xiàn),每個(gè)節(jié)點(diǎn)的梯度都可以看做上一個(gè)節(jié)點(diǎn)的梯度與本層導(dǎo)數(shù)的乘積。因此,計(jì)算圖網(wǎng)絡(luò)結(jié)構(gòu)建立的同時(shí),每個(gè)節(jié)點(diǎn)的梯度也是確定了的。

全連接層

說到全連接層,就不得不提一下BP神經(jīng)網(wǎng)絡(luò)。神經(jīng)網(wǎng)絡(luò)是模方大腦神經(jīng)元的連接方式,通過建立神經(jīng)元之間的相互聯(lián)系來(lái)對(duì)某個(gè)問題進(jìn)行建模的方法。BP神經(jīng)網(wǎng)絡(luò)是在眾多神經(jīng)網(wǎng)絡(luò)中一枝獨(dú)秀,利用梯度下降方法,可以最快最準(zhǔn)的接近局部最優(yōu)值(缺點(diǎn)當(dāng)然是容易陷入局部最優(yōu),因此也出現(xiàn)了很多改進(jìn)算法)。

從最簡(jiǎn)單的線性回歸說起。我們有兩組數(shù)據(jù),自變量:x,目標(biāo):t。對(duì)目標(biāo)t的估計(jì)值為:

估計(jì)值和目標(biāo)之間的均方誤差為:

可以做出loss關(guān)于a0和a1的三維曲面:

可以看到他是一個(gè)二次函數(shù)形式,必然存在最小值。我們對(duì)a0和a1分別求偏導(dǎo):

最小二乘法是利用這兩個(gè)偏導(dǎo)數(shù)等于0求出取到最值時(shí)的a0和a1:

而梯度下降法則是將a0和a1按照上面算出的導(dǎo)數(shù),以某個(gè)速率向最值點(diǎn)靠近(以他們的導(dǎo)數(shù)乘某個(gè)常數(shù)為速率,以梯度的負(fù)數(shù)為方向,做爬坡運(yùn)動(dòng))

對(duì)于線性回歸問題來(lái)說,loss是個(gè)拋物線,必然存在最值點(diǎn):

但是對(duì)于更復(fù)雜的問題來(lái)說,就不一定了,loss不一定是拋物線,可能存在多個(gè)極值點(diǎn),這樣的話最小二乘法便不再好用,但是梯度下降卻可以盡最大可能搜索最優(yōu)值(雖然不容易跳出局部最優(yōu),但還是比最小二乘法要好吧)

但是隨著問題復(fù)雜度的提升,自變量和因變量之間也不再是簡(jiǎn)單的線性關(guān)系,這時(shí)就要加入一個(gè)激活函數(shù),但是光加入激活函數(shù)只能表示一些簡(jiǎn)單的非線性模型,對(duì)于更加復(fù)雜的非線性模型也無(wú)能為力。

所以現(xiàn)在的問題是如何利用簡(jiǎn)單的方程表示出復(fù)雜的方程。這一點(diǎn)在高等數(shù)學(xué)中其實(shí)已經(jīng)學(xué)到了。對(duì)于一個(gè)復(fù)雜的函數(shù),我們總可以把他展開成級(jí)數(shù)的形式,而且我們還可以證明,展開的級(jí)數(shù)就是泰勒級(jí)數(shù),這個(gè)展開也叫做泰勒展開。

我們可以利用最簡(jiǎn)單的冪函數(shù)的疊加來(lái)趨近一個(gè)極其復(fù)雜的函數(shù)。那在這個(gè)問題上,也可以利用無(wú)數(shù)個(gè)簡(jiǎn)單的非線性函數(shù)的疊加來(lái)擬合一個(gè)極其復(fù)雜的非線性函數(shù),所以就有了下圖:

把n個(gè)線性關(guān)系疊加起來(lái)再通過激活函數(shù)計(jì)算得到最終的估計(jì)值:

損失值loss(均方差)為:

要繼續(xù)利用梯度下降法,就需要計(jì)算loss關(guān)于每個(gè)w的導(dǎo)數(shù):

這樣就建立了梯度下降與鏈?zhǔn)椒▌t之間聯(lián)系,和上一文《簡(jiǎn)單實(shí)現(xiàn)自動(dòng)求導(dǎo)》一模一樣。

這里可以體現(xiàn)出計(jì)算圖的好處。我們不需要再手動(dòng)計(jì)算這些復(fù)雜的梯度值到底是多少,在計(jì)算圖建立的同時(shí),它就已經(jīng)幫我們算好了。我們只需要給他一個(gè)學(xué)習(xí)的指令,他就可以按照我們要求的訓(xùn)練方式學(xué)習(xí)。

全連接層,顧名思義,重在“全連接”,他將所有的輸入都和輸出相連,如下圖的兩層神經(jīng)網(wǎng)絡(luò),一個(gè)輸入,一個(gè)隱含層,一個(gè)輸出。

在黑框中可以看到,所有的輸入X都和所有隱含層的節(jié)點(diǎn)相連,所有隱含層的節(jié)點(diǎn)都和輸出y相連。

或許會(huì)問,難道還有不全連接的神經(jīng)網(wǎng)絡(luò)嗎。當(dāng)然有,先不說一些復(fù)雜的結(jié)果,最基本的卷積神經(jīng)網(wǎng)絡(luò)CNN就不是全連接的,它利用卷積核掃描圖像,而卷積核一般都是比圖像小的多的方形如3×3,5×5。

兩層的神經(jīng)網(wǎng)絡(luò)理論上可以擬合任意的非線性函數(shù)。在梯度計(jì)算方面與之前稍有不同:先看第一層,第j個(gè)輸出為:

要計(jì)算w的該變量,就需要計(jì)算前面那個(gè)鏈?zhǔn)奖磉_(dá)式,它其中的一項(xiàng):

所以:

對(duì)于很多層的網(wǎng)絡(luò):

我們給定一個(gè)變量δ,它實(shí)際上是每一層神經(jīng)網(wǎng)絡(luò)的梯度,下式為輸出層的梯度:

那么權(quán)重改變量:

而上一層的梯度:

權(quán)重改變量:

因此,在網(wǎng)絡(luò)訓(xùn)練過程中,先計(jì)算每一層的梯度和權(quán)重改變量,算完之后再對(duì)每一層的權(quán)重梯度下降。值得注意的是:不可以每計(jì)算一層的權(quán)重改變量,就對(duì)該層的權(quán)重做出改變。這樣會(huì)影響后面的計(jì)算。

寫到這兒,需要的理論知識(shí)已經(jīng)有了。接下來(lái)就是如何搭建靜態(tài)圖:

上一文有提到,我搭建的計(jì)算圖中只有兩個(gè)基本變量:Variable和placeholder。

placeholder作為需要傳入數(shù)據(jù)的變量,Variable則作為需要訓(xùn)練的變量(這里用不到不需要訓(xùn)練的變量,不然應(yīng)該再給一個(gè)Constant更完整,我這里其實(shí)簡(jiǎn)化了很多操作^_^)

與之前相同的是,Variable包括value和grad兩個(gè)變量。

不同的是,Variable包含了last,next,root和target四個(gè)指針(可以這么叫吧)。last指向前一個(gè)節(jié)點(diǎn),next指向后一個(gè)節(jié)點(diǎn),root指向輸入的placeholder(就是起始節(jié)點(diǎn)),target指向目標(biāo)的placeholder(要計(jì)算loss,就必須要計(jì)算輸出和目標(biāo)之間的差距),一條路徑上的每個(gè)節(jié)點(diǎn)的root和target都相同(這是為了方便對(duì)每個(gè)節(jié)點(diǎn)求值的時(shí)候都能從placeholder開始)。

placeholder在圖中只起到占位的作用,它用來(lái)確定計(jì)算內(nèi)存與復(fù)雜度,需要在后續(xù)求值的時(shí)候傳入數(shù)據(jù)。每個(gè)節(jié)點(diǎn)都有root指針,可以確保在求該節(jié)點(diǎn)值的時(shí)候,從它的root開始,先傳入數(shù)據(jù),然后利用next指針指向下一個(gè)節(jié)點(diǎn),計(jì)算下一個(gè)節(jié)點(diǎn)的值和導(dǎo)數(shù)。

值得一提的是,這里我沒有使用一個(gè)全局變量來(lái)儲(chǔ)存整個(gè)圖結(jié)構(gòu),而是用上述的四個(gè)指針將整個(gè)計(jì)算圖建立起來(lái)。類似于{root,next1(last1),next2(last2)。。。target}。

如果不使用root指針,還有兩個(gè)方法:

1.儲(chǔ)存整個(gè)網(wǎng)絡(luò)的圖結(jié)構(gòu),這也是一般計(jì)算圖的方案,這樣就可以從圖的起始節(jié)點(diǎn)開始算起。

2.要求一個(gè)節(jié)點(diǎn)的值,就要先知道上一個(gè)節(jié)點(diǎn)的值,然后帶入上一個(gè)節(jié)點(diǎn)的激活函數(shù)才能得到這個(gè)節(jié)點(diǎn)的值。同理要知道前一個(gè)節(jié)點(diǎn)的值,就要求上上個(gè)節(jié)點(diǎn)的值,以此類推。而當(dāng)上上一個(gè)節(jié)點(diǎn)是placeholder時(shí),上上個(gè)節(jié)點(diǎn)值就是傳入的數(shù)據(jù),然后再計(jì)算上個(gè)節(jié)點(diǎn)的值。這不正是遞歸算法嘛,先往回走,走到初值時(shí),再往前走。那用一個(gè)root指針豈不是取代了往回走的過程,我直接告訴它該從哪里開始往前走即可。

下面放入代碼:

先說明一下整個(gè)代碼的結(jié)構(gòu):

mytensor文件夾中包含五個(gè)py文件,__init__用于從tensor文件夾外部調(diào)用內(nèi)部的文件。mytensor(這里不是指文件夾,而是mytensor文件夾內(nèi)的py文件)用來(lái)定義基類Varibale,placeholder,一些基本函數(shù),還有不同的Loss。nn用于定義不同的神經(jīng)網(wǎng)絡(luò)。train用于定義不同的訓(xùn)練方式。cnn_func包含卷積中的一些操作(這里不會(huì)提到)。

代碼文件mytensor.py

# -*- coding: utf-8 -*- import numpy as np #Variable基類 class Variable:def __init__(self, value=None):self.value = valueself.grad = Noneself.next = Noneself.last = Noneself.root = None self.target = Noneself.eval_func = Lineif isinstance(value, np.ndarray):self.size = value.shapeelse:self.size = None def __add__(self, other):res = Variable()res.root = self.rootself.next, other.next = res, resres.last = selfres.func = lambda x: x + other.valueres.func_grad = lambda x: 1return resdef func(self, X):return 1def func_grad(self, X):return 1def run(self, feed_dict, need_grad=False):#喂入數(shù)據(jù)root = self.root#找到rootroot.value = feed_dict[root]#給root喂入數(shù)據(jù)target = self.target#找到targetif target is not None:target.value = feed_dict[target]#給target喂入數(shù)據(jù)#求值過程while root.next is not self.next: root.next.value = root.next.func(root.value)if need_grad:#如果需要求導(dǎo)root.next.grad = root.grad * root.next.func_grad(root.value)root = root.nextreturn root.value

__add__函數(shù)用來(lái)定義兩個(gè)Variable的相加算法。這里Variable多出了eval_func變量,雖然這里用不到,但后續(xù)有類繼承它時(shí)才會(huì)用到。

run方法便是在求該節(jié)點(diǎn)值得時(shí)候會(huì)用到,與之前所說相同,先找到節(jié)點(diǎn)的root,然后從root開始一步一步往前傳,如果需要自動(dòng)求導(dǎo)的話,need_grad傳入True即可。

定義完基類,接下來(lái)就是繼承基類的子類了,首先是placeholder:

###############占位##################### class placeholder(Variable):def __init__(self, size):super().__init__(self)self.size = sizeself.root = selfself.grad = 1

然后定義一些全連接層中可能用到的激活函數(shù):

class relu(Variable):def __init__(self, X):super().__init__()X.next = selfself.last = Xself.root = X.rootdef func(self, X):return np.maximum(0, X)def func_grad(self, X):res = np.zeros(X.shape)res[X >= 0] = 1return resclass Line(Variable):def __init__(self, X):super().__init__(self)self.last = Xself.root = X.rootdef func(self, X):return Xdef func_grad(self, X):return np.ones(X.shape)class softmax(Variable):def __init__(self, X):super().__init__(self)X.next = selfself.last = Xself.root = X.rootdef func(self, X):return np.exp(X) / np.sum(np.exp(X), axis = 1).reshape(X.shape[0], 1)def func_grad(self, X):return np.ones(X.shape) class sigmoid(Variable):def __init__(self, X):super().__init__(self)X.next = selfself.last = Xself.root = X.rootdef func(self, X):return 1 / (1 + np.exp(-X))def func_grad(self, X):return self.func(X) * (1 - self.func(X)) class square(Variable):def __init__(self, X):super().__init__(self)X.next = selfself.last = Xself.root = X.rootdef func(self, X):return np.square(X)def func_grad(self, X):return 2 * X

寫法與上一文自動(dòng)求導(dǎo)中定義初等函數(shù)的方法完全一致(多了一個(gè)next指針)。

然后定義損失函數(shù):

class MeanSquareLoss(Variable):def __init__(self, yhat, y):super().__init__(self)self.target = yself.last = yhatself.root = yhat.rootyhat.next = selfdef func(self, yhat):return np.mean(np.square(yhat - self.target.value)) / 2def func_grad(self, yhat, grad):return (self.target.value - yhat) * gradclass SoftmaxCrossEntropy(Variable):def __init__(self, yhat, y):super().__init__(self)self.target = yself.last = yhatself.root = yhat.rootyhat.next = selfdef func(self, yhat):return np.mean(-np.log(yhat) * self.target.value)def func_grad(self, yhat, grad): return (self.target.value - yhat)

這里不定義損失函數(shù)也是可以的。但是還需要用定義初等函數(shù)的方法寫一下mean函數(shù)(square函數(shù)我倒是寫了)。然后在訓(xùn)練的時(shí)候,可以把loss = MeanSquareLoss(yhat,y)寫為loss = mean(square(yhat - y))

然后是Session。其實(shí)我這里寫Session只是為了形式上好看點(diǎn),失去了tensorflow中Session的意義。。

###############Session#################### class Session:def run(self, operator, feed_dict, need_grad = False):return operator.run(feed_dict, need_grad)

上述代碼全部包含在mytensor.py中。

在nn.py文件中,就開始構(gòu)建真正的全連接層了:

# -*- coding: utf-8 -*- #導(dǎo)入一些要用到的函數(shù) from mytensor.mytensor import Variable import mytensor.mytensor as mt import numpy as np #全連接層 class FullConnection(Variable):def __init__(self, X, W, b=None, eval_func=None, need_trans=False):super().__init__(self)X.next = selfself.last = Xself.root = X.rootself.W = Wif b == None:self.b = Variable(np.zeros(W.size[1]))else:self.b = bself.eval_func = eval_funcself.need_trans = need_transdef func(self, X):if self.need_trans:N = X.shape[0]D = np.prod(X.shape[1:])X = np.reshape(X, (N, D))h = X.dot(self.W.value) + self.b.valueif self.eval_func is None:self.eval_func = mt.Lineh = self.eval_func(self.W).func(h)self.value = hreturn hdef func_grad(self, dout, first=False):if first:grad = self.eval_func(self.W).func_grad(self.value)return gradelse: dw = self.last.value.T.dot(dout)db = np.sum(dout, axis = 0)grad = self.last.eval_func(self.W).func_grad(self.last.value)dout = np.reshape(dout.dot(self.W.value.T), self.last.value.shape) * grad return dout, dw, db

可以看到全連接層的構(gòu)造和之前的基本函數(shù)大的框架是一致的。構(gòu)造函數(shù)中要有父類的構(gòu)造函數(shù),然后重寫func和func_grad。

不同之處在于,全連接層的變量更多,有權(quán)重W,偏置b,激活函數(shù)eval_func。need_trans是針對(duì)卷積操作時(shí)判斷是否需要把輸入的矩陣轉(zhuǎn)化為一個(gè)長(zhǎng)鏈的布爾值。

func函數(shù)用來(lái)計(jì)算節(jié)點(diǎn)的值,就是基本的f(Xw+b)。

而func_grad輸入上一層的梯度值,計(jì)算本層的梯度值以及權(quán)重和偏置的改變量,并返回。

不管是全連接層還是CNN,RNN。網(wǎng)絡(luò)構(gòu)造都是這個(gè)模式。構(gòu)造函數(shù)中儲(chǔ)存所有的變量。func中是前傳操作,func_grad中利用上一層的梯度,計(jì)算本層的權(quán)重,偏置該變量和本層的梯度。

這樣便可以將不同的網(wǎng)絡(luò)結(jié)構(gòu)統(tǒng)一起來(lái),讓后續(xù)工作簡(jiǎn)單化。

現(xiàn)在可以說是萬(wàn)事俱備只欠東風(fēng)了。該定義的都定義了,該搭建的也都搭建好了,我們已經(jīng)可以搭建出任意層數(shù)的神經(jīng)網(wǎng)絡(luò),并且定義它的loss值。可以認(rèn)為:前傳過程已經(jīng)沒有任何問題了。但是現(xiàn)在需要個(gè)一個(gè)東西讓他們跑起來(lái)——訓(xùn)練。

train.py中只定義了GradientDecent訓(xùn)練方式:

# -*- coding: utf-8 -*- #訓(xùn)練方式 from mytensor.mytensor import Variable #梯度下降法 class GradientDescentOptimizer(Variable):def __init__(self, alpha, loss):super().__init__()self.alpha = alphaself.loss = lossdef run(self, feed_dict, end=False):now = self.lossnow.run(feed_dict)dnow = now.last now.grad = [now.func_grad(dnow.value, dnow.func_grad(None, True))]while dnow is not now.root:dnow.grad = dnow.func_grad(now.grad[0])now, dnow = dnow, dnow.last root = self.loss.rootwhile root.next is not self.loss:try:root.next.W.value += self.alpha * root.next.grad[1]root.next.b.value += self.alpha * root.next.grad[2]except:passroot = root.next

它和別的類不同,沒有func和func_grad。但它卻重寫了run方法。畢竟它是優(yōu)化器,與之前的節(jié)點(diǎn)都不同(其實(shí)這里不需要繼承父類)。

這里首先要計(jì)算完每一層的梯度,每一層權(quán)重和偏置的改變量,然后才可以對(duì)權(quán)重和偏置做出改變,否則會(huì)影響每一層梯度的計(jì)算。本質(zhì)上這些計(jì)算得是并行的,但是由于本層的梯度必須要利用前一層的梯度,擁有串行的性質(zhì)。這就導(dǎo)致了我們必須得寫兩個(gè)循環(huán)。

使用try-except是因?yàn)?#xff0c;卷積中池化層沒有權(quán)重和偏置,為了程序的統(tǒng)一才用這個(gè)方法。

這樣所有的方法都已經(jīng)寫好,如果從mytensor文件夾外調(diào)用的就需要__init__.py了。

__init__.py中定義了接口的調(diào)用方式

# -*- coding: utf-8 -*- from .nn import FullConnection from .nn import Conv2D from .nn import MaxPool from .train import GradientDescentOptimizer from .mytensor import Variable, placeholder from .mytensor import matmul from .mytensor import exp, sin, cos, log from .mytensor import square, relu, softmax, sigmoid, Line from .mytensor import MeanSquareLoss, SoftmaxCrossEntropy from .mytensor import Session

用mnist數(shù)據(jù)集測(cè)試:

# -*- coding: utf-8 -*- ''' mnist測(cè)試 ''' import numpy as np import mytensor as mt from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import LabelBinarizer from sklearn import metricsdata = np.load("mnist.npz") def DataStandard(X):return StandardScaler().fit_transform(X) def DataTrans(y):return LabelBinarizer().fit_transform(y)X_train, y_train, X_val, y_val, X_test, y_test = data['X'], data['y'], data['X_val'], data['y_val'], data['X_test'], data['y_test'] X_train, X_val, X_test = DataStandard(X_train), DataStandard(X_val), DataStandard(X_test) y_train, y_val, y_test = DataTrans(y_train), DataTrans(y_val), DataTrans(y_test) BATCH_SIZE = 128 #權(quán)重和偏置 W1 = mt.Variable(np.random.uniform(-0.01, 0.01, (784, 128))) b1 = mt.Variable(np.random.uniform(-0.01, 0.01, (128))) W2 = mt.Variable(np.random.uniform(-0.01, 0.01, (128, 10))) b2 = mt.Variable(np.random.uniform(-0.01, 0.01, (10))) #占位符 xs = mt.placeholder((None, 784)) ys = mt.placeholder((None, 10)) #兩層神經(jīng)網(wǎng)絡(luò) h1 = mt.nn.FullConnection(xs, W1, b1, mt.relu) h2 = mt.nn.FullConnection(h1, W2, b2, mt.softmax) #定義loss和train loss = mt.SoftmaxCrossEntropy(h2, ys) train = mt.train.GradientDescentOptimizer(1e-4, loss)sess = mt.Session()start = 0 for i in range(10000): end = start + BATCH_SIZEif end >= X_train.shape[0]:end = X_train.shape[0] - 1X_batch = X_train[start: end]y_batch = y_train[start: end]start = endif start == X_train.shape[0] - 1:start = 0sess.run(train, {xs: X_batch, ys: y_batch})if (i % 100 == 0):los = sess.run(loss, {xs: X_val, ys: y_val})output = sess.run(h2, {xs: X_val})y_pred = np.argmax(output, axis = 1)acc = metrics.accuracy_score(y_pred, np.argmax(y_val, axis = 1))print("times : {}, loss : {}, accuracy : {}".format(i, los, acc))los = sess.run(loss, {xs: X_test, ys: y_test}) output = sess.run(h2, {xs: X_test}) y_pred = np.argmax(output, axis = 1) acc = metrics.accuracy_score(y_pred, np.argmax(y_test, axis = 1)) print("test data: loss : {}, accuracy : {}".format(los, acc))

。。。

最后得到93%正確率。其實(shí)這在全連接層還算是可以接受的結(jié)果。如果利用卷積池化來(lái)處理的話,準(zhǔn)確率必然會(huì)大大提高。

后記:數(shù)模又雙叒叕參與獎(jiǎng)了,懷著悲憤的心情才抽出時(shí)間寫下此文。

總結(jié)

以上是生活随笔為你收集整理的全连接层的作用_python构建计算图2——全连接层的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。