自己动手做聊天机器人 四十二-(重量级长文)从理论到实践开发自己的聊天机器人

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

什么是神经网络

这本小书一定要看:《Make Your Own Neural Network》,老外写的,200多页,没找到中文译本,原版百度一下可以下载到,他用非常通俗易懂的描述讲解了人工神经网络的原理并用代码实现,而且试验效果非常好,作为深度学习入门非常值得推荐。

 

什么是循环神经网络和LSTM

可以参考我的这篇文章自己动手做聊天机器人 二十六-图解递归神经网络(RNN),或者直接看Christopher Olah的http://colah.github.io/posts/2015-08-Understanding-LSTMs/,这篇博文被业界引用无数次了,经典中的经典。

 

什么是seq2seq模型

seq2seq是基于循环神经网络的一种序列到序列模型,语言翻译、自动问答等都属于序列到序列的场景,都可以使用seq2seq模型,用seq2seq实现聊天机器人的原理可以看下这篇文章http://suriyadeepan.github.io/2016-06-28-easy-seq2seq/。tensorflow中已经有实现好的api供我们使用,但因为参数较多、原理复杂,理解起来比较晦涩,本文就让我带着大家一步一步探索并使用。

 

什么是attention模型

attention模型(注意力模型)是为了解决seq2seq中解码器只接受编码器最后一个输出而远离了之前的输出导致的信息丢失的问题,从原理上讲,一个回答一般是基于问题中一些关键位置的信息,也就是注意力集中的地方,具体细节可以看下http://www.wildml.com/2016/01/attention-and-memory-in-deep-learning-and-nlp/

 

用tensorflow的seq2seq制作你自己的聊天机器人

到这里,假设你已经掌握了上面的理论部分,现在我们直接锁定在tensorflow提供给我们的强大api上。想真正利用好tensorflow必须理解好它的重要接口及其所有参数,所以第一步我们找到我们这次要使用的最关键的接口:https://www.tensorflow.org/api_docs/python/tf/contrib/legacy_seq2seq/embedding_attention_seq2seq

embedding_attention_seq2seq(
    encoder_inputs,
    decoder_inputs,
    cell,
    num_encoder_symbols,
    num_decoder_symbols,
    embedding_size,
    num_heads=1,
    output_projection=None,
    feed_previous=False,
    dtype=None,
    scope=None,
    initial_state_attention=False
)

为了说明这个接口的功能,先来看一下它所涉及到的模型结构,如下所示:

参数encoder_inputs是一个list,list中每一项是1D的Tensor,这个Tensor的shape是[batch_size],Tensor中每一项是一个整数,类似这样:

[array([0, 0, 0, 0], dtype=int32), 
array([0, 0, 0, 0], dtype=int32), 
array([8, 3, 5, 3], dtype=int32), 
array([7, 8, 2, 1], dtype=int32), 
array([6, 2, 10, 9], dtype=int32)]

其中的5个array,表示一句话的长度是5个词

其中每个array里有4个数,表示batch是4,也就是一共4个样本。那么可以看出第一个样本是[[0],[0],[8],[7],[6]],第二个样本是[[0],[0],[3],[8],[2]],这里的数字是用来区分不同词的一个id,一般通过统计得出,一个id表示一个词

同理,参数decoder_inputs也是和encoder_inputs一样结构,不赘述

参数cell是tf.nn.rnn_cell.RNNCell类型的循环神经网络单元,可以用tf.contrib.rnn.BasicLSTMCell、tf.contrib.rnn.GRUCell

参数num_encoder_symbols是一个整数,表示encoder_inputs中的整数词id的数目,同理num_decoder_symbols表示decoder_inputs中整数词id的数目

embedding_size表示在内部做word embedding时转成几维向量,需要和RNNCell的size大小相等

num_heads表示在attention_states中的抽头数量

output_projection是一个(W, B)结构的tuple,W是shape为[output_size x num_decoder_symbols]的weight矩阵,B是shape为[num_decoder_symbols]的偏置向量,那么每个RNNCell的输出经过WX+B就可以映射成num_decoder_symbols维的向量,这个向量里的值表示的是任意一个decoder_symbol的可能性,也就是softmax

feed_previous表示decoder_inputs是我们直接提供训练数据的输入,还是用前一个RNNCell的输出映射出来的,如果feed_previous为True,那么就是用前一个RNNCell的输出,并经过WX+B映射成

dtype是RNN状态数据的类型,默认是tf.float32

scope是子图的命名,默认是“embedding_attention_seq2seq”

initial_state_attention表示是否初始化attentions,默认为否,表示全都初始化为0

它的返回值是一个(outputs, state)结构的tuple,其中outputs是一个长度为句子长度(词数,与上面encoder_inputs的list长度一样)的list,list中每一项是一个2D的tf.float32类型的Tensor,第一维度是样本数,比如4个样本则有四组Tensor,每个Tensor长度是embedding_size,像下面的样子:

  [
    array([
      [-0.02027004, -0.017872  , -0.00233014, -0.0437047 ,  0.00083584,
      0.01339234,  0.02355197,  0.02923143],
      [-0.02027004, -0.017872  , -0.00233014, -0.0437047 ,  0.00083584,
      0.01339234,  0.02355197,  0.02923143],
      [-0.02027004, -0.017872  , -0.00233014, -0.0437047 ,  0.00083584,
      0.01339234,  0.02355197,  0.02923143],
      [-0.02027004, -0.017872  , -0.00233014, -0.0437047 ,  0.00083584,
      0.01339234,  0.02355197,  0.02923143]
    ],dtype=float32),
    array([
    ......
    ],dtype=float32),  
    array([
    ......
    ],dtype=float32),  
    array([
    ......
    ],dtype=float32),  
    array([
    ......
    ],dtype=float32),  
  ]

其实这个outputs可以描述为5*4*8个浮点数,5是句子长度,4是样本数,8是词向量维数

下面再看返回的state,它是num_layers个LSTMStateTuple组成的大tuple,这里num_layers是初始化cell时的参数,表示神经网络单元有几层,一个由3层LSTM神经元组成的encoder-decoder多层循环神经网络是像下面这样的网络结构:

encoder_inputs输入encoder的第一层LSTM神经元,这个神经元的output传给第二层LSTM神经元,第二层的output再传给第三层,而encoder的第一层输出的state则传给decoder第一层的LSTM神经元,依次类推,如上图所示

回过头来再看LSTMStateTuple这个结构,它是由两个Tensor组成的tuple,第一个tensor命名为c,由4个8维向量组成(4是batch, 8是state_size也就是词向量维度), 第二个tensor命名为h,同样由4个8维向量组成

这里的c和h如下所示:

c是传给下一个时序的存储数据,h是隐藏层的输出,这里的计算公式是

在tensorflow代码里也有对应的实现:

concat = _linear([inputs, h], 4 * self._num_units, True)
i, j, f, o = array_ops.split(value=concat, num_or_size_splits=4, axis=1)
new_c = (c * sigmoid(f + self._forget_bias) + sigmoid(i) * self._activation(j))
new_h = self._activation(new_c) * sigmoid(o)

事实上,如果我们直接使用embedding_attention_seq2seq来做训练,返回的state一般是用不到的

下面,我们就来想办法构造这些输入参数来训练一个seq2seq模型出来

我们以1、3、5、7、9……奇数序列为例来构造样本,比如两个样本是[[1,3,5],[7,9,11]]和[[3,5,7],[9,11,13]],相当于我们的:

train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]

为了我们能够满足不同长度的序列,需要让我们训练的序列比样本的序列长度要长一些,比如我们设置为5,即

input_seq_len = 5
output_seq_len = 5

因为样本长度小于训练序列的长度,所以我们用0来填充,即

PAD_ID = 0

那么我们的第一个样本的encoder_input就是:

encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0])) + train_set[0][0]

第二个样本的encoder_input就是:

encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0])) + train_set[1][0]

decoder_input我们需要用一个GO_ID来作为起始,再输入样本序列,最后再用PAD_ID来填充,即

GO_ID = 1
decoder_input_0 = [GO_ID] + train_set[0][1] 
    + [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1)
decoder_input_1 = [GO_ID] + train_set[1][1] 
    + [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1)

为了把输入转成上面讲到的embedding_attention_seq2seq输入参数encoder_inputs和decoder_inputs的格式,我们进行如下转换即可:

encoder_inputs = []
decoder_inputs = []
for length_idx in xrange(input_seq_len):
    encoder_inputs.append(np.array([encoder_input_0[length_idx], 
                          encoder_input_1[length_idx]], dtype=np.int32))
for length_idx in xrange(output_seq_len):
    decoder_inputs.append(np.array([decoder_input_0[length_idx], 
                          decoder_input_1[length_idx]], dtype=np.int32))

运行已经写完的这段程序,打印出encoder_inputs和decoder_inputs如下:

[
  array([0, 0], dtype=int32), 
  array([0, 0], dtype=int32), 
  array([ 1, 23], dtype=int32), 
  array([ 3, 25], dtype=int32), 
  array([ 5, 27], dtype=int32)
]
[
  array([1, 1], dtype=int32), 
  array([ 7, 29], dtype=int32), 
  array([ 9, 31], dtype=int32), 
  array([11, 33], dtype=int32), 
  array([0, 0], dtype=int32)
]

好,第一步大功告成,我们把这部分独立出一个函数,整理代码如下:

# coding:utf-8
import numpy as np
# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1

def get_samples(): “““构造样本数据 :return: encoder_inputs: [array([0, 0], dtype=int32), array([0, 0], dtype=int32), array([1, 3], dtype=int32), array([3, 5], dtype=int32), array([5, 7], dtype=int32)] decoder_inputs: [array([1, 1], dtype=int32), array([7, 9], dtype=int32), array([ 9, 11], dtype=int32), array([11, 13], dtype=int32), array([0, 0], dtype=int32)] "”” train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]] encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0])) + train_set[0][0] encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0])) + train_set[1][0] decoder_input_0 = [GO_ID] + train_set[0][1] + [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1) decoder_input_1 = [GO_ID] + train_set[1][1] + [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1) encoder_inputs = [] decoder_inputs = [] for length_idx in xrange(input_seq_len): encoder_inputs.append(np.array([encoder_input_0[length_idx], encoder_input_1[length_idx]], dtype=np.int32)) for length_idx in xrange(output_seq_len): decoder_inputs.append(np.array([decoder_input_0[length_idx], decoder_input_1[length_idx]], dtype=np.int32)) return encoder_inputs, decoder_inputs

搞定上面这部分之后,我们开始构造模型,我们了解了tensorflow的运行过程是先构造图,再塞数据计算的,所以我们构建模型的过程实际上就是构建一张图,

首先我们创建encoder_inputs和decoder_inputs的placeholder(占位符):

import tensorflow as tf
encoder_inputs = []
decoder_inputs = []
for i in xrange(input_seq_len):
    encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="encoder{0}".format(i)))
for i in xrange(output_seq_len):
    decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="decoder{0}".format(i)))

接下来我们创建一个记忆单元数目为size=8的LSTM神经元结构:

size = 8
cell = tf.contrib.rnn.BasicLSTMCell(size)

我们假设我们要训练的奇数序列最大数值是输入最大10输出最大16,那么

num_encoder_symbols = 10
num_decoder_symbols = 16

然后把参数传入embedding_attention_seq2seq获取output

from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq
outputs, _ = seq2seq.embedding_attention_seq2seq(
                    encoder_inputs,
                    decoder_inputs[:output_seq_len],
                    cell,
                    cell,
                    num_encoder_symbols=num_encoder_symbols,
                    num_decoder_symbols=num_decoder_symbols,
                    embedding_size=size,
                    output_projection=None,
                    feed_previous=False,
                    dtype=tf.float32)

为了说明之后的操作,我们先把这部分运行一下,看看输出的output是个什么样的数据,我们先把上面的构建模型部分放到一个单独的函数里,如下:

def get_model():
    """构造模型
    """
    encoder_inputs = []
    decoder_inputs = []
    for i in xrange(input_seq_len):
        encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="encoder{0}".format(i)))
    for i in xrange(output_seq_len):
        decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], 
                          name="decoder{0}".format(i)))
    cell = tf.contrib.rnn.BasicLSTMCell(size)
    # 这里输出的状态我们不需要
    outputs, _ = seq2seq.embedding_attention_seq2seq(
                        encoder_inputs,
                        decoder_inputs,
                        cell,
                        num_encoder_symbols=num_encoder_symbols,
                        num_decoder_symbols=num_decoder_symbols,
                        embedding_size=size,
                        output_projection=None,
                        feed_previous=False,
                        dtype=tf.float32)
    return encoder_inputs, decoder_inputs, outputs

来构造运行时的session,并填入样本数据:

with tf.Session() as sess:
    sample_encoder_inputs, sample_decoder_inputs = get_samples()
    encoder_inputs, decoder_inputs, outputs = get_model()
    input_feed = {}
    for l in xrange(input_seq_len):
        input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
    for l in xrange(output_seq_len):
        input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
    sess.run(tf.global_variables_initializer())
    outputs = sess.run(outputs, input_feed)
    print outputs

我们看到这里输出的outputs是由5个array组成的list(5是序列长度),每个array由两个size是16的list组成(2表示2个样本,16表示输出符号有16个)

这里的outputs实际上应该对应seq2seq的输出,也就是下图中的W、X、Y、Z、EOS,也就是decoder_inputs[1:],也就是我们样本里的[7,9,11]和[9,11,13]

但是我们的decoder_inputs的结构是这样的:

[array([1, 1], dtype=int32), array([ 7, 29], dtype=int32), array([ 9, 31], dtype=int32), array([11, 33], dtype=int32), array([0, 0], dtype=int32)]

与这里的outputs稍有不同,所以不是直接的对应关系,那么到底是什么关系呢?我们先来看一个损失函数的说明:

https://www.tensorflow.org/api_docs/python/tf/contrib/legacy_seq2seq/sequence_loss

sequence_loss(
    logits,
    targets,
    weights,
    average_across_timesteps=True,
    average_across_batch=True,
    softmax_loss_function=None,
    name=None
)

这个函数的原理可以看这个公式(损失函数,目标词语的平均负对数概率最小):

其中logits是一个由多个2D的shape为[batch * num_decoder_symbols]的Tensor组成的list,我们这里batch就是2,num_decoder_symbols就是16,这里组成list的Tensor的个数是output_seq_len,所以我们刚才得到的outputs刚好符合

其中targets是一个和logits一样长度(output_seq_len)的list,list里每一项是一个整数组成的1D的Tensor,每个Tensor的shape是[batch],数据类型是tf.int32,这刚好和我们的decoder_inputs[1:]也就是刚才说的W、X、Y、Z、EOS结构一样

其中weights是一个和targets结构一样,只是数据类型是tf.float32

所以这个函数就是用来计算加权交叉熵损失的,这里面的weights我们需要初始化他的占位符,如下:

target_weights = []
    target_weights.append(tf.placeholder(tf.float32, shape=[None], 
                          name="weight{0}".format(i)))

那么我们计算得出的损失值就是:

targets = [decoder_inputs[i + 1] for i in xrange(len(decoder_inputs) - 1)]
loss = seq2seq.sequence_loss(outputs, targets, target_weights)

看到这里,其实我们遇到一个问题,这里的targets长度比decoder_inputs少了一个,为了让长度保持一致,需要我们对前面decoder_inputs的初始化做个调整,把长度加1

那么问题来了,这里我们多了一个target_weights这个placeholder,那么我们用什么数据来填充这个占位符呢?因为我们要计算的是加权交叉熵损失,也就是对于有意义的数权重大,无意义的权重小,所以我们把targets中有值的赋值为1,没值的赋值为0,所有代码整理后如下:

# coding:utf-8
import numpy as np
import tensorflow as tf
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq
# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# LSTM神经元size
size = 8
# 最大输入符号数
num_encoder_symbols = 10
# 最大输出符号数
num_decoder_symbols = 16

def get_samples(): “““构造样本数据 :return: encoder_inputs: [array([0, 0], dtype=int32), array([0, 0], dtype=int32), array([1, 3], dtype=int32), array([3, 5], dtype=int32), array([5, 7], dtype=int32)] decoder_inputs: [array([1, 1], dtype=int32), array([7, 9], dtype=int32), array([ 9, 11], dtype=int32), array([11, 13], dtype=int32), array([0, 0], dtype=int32)] "”” train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]] encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0])) + train_set[0][0] encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0])) + train_set[1][0] decoder_input_0 = [GO_ID] + train_set[0][1] + [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1) decoder_input_1 = [GO_ID] + train_set[1][1] + [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1) encoder_inputs = [] decoder_inputs = [] target_weights = [] for length_idx in xrange(input_seq_len): encoder_inputs.append(np.array([encoder_input_0[length_idx], encoder_input_1[length_idx]], dtype=np.int32)) for length_idx in xrange(output_seq_len): decoder_inputs.append(np.array([decoder_input_0[length_idx], decoder_input_1[length_idx]], dtype=np.int32)) target_weights.append(np.array([ 0.0 if length_idx == output_seq_len - 1 or decoder_input_0[length_idx] == PAD_ID else 1.0, 0.0 if length_idx == output_seq_len - 1 or decoder_input_1[length_idx] == PAD_ID else 1.0, ], dtype=np.float32)) return encoder_inputs, decoder_inputs, target_weights

def get_model(): “““构造模型 "”” encoder_inputs = [] decoder_inputs = [] target_weights = [] for i in xrange(input_seq_len): encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], name=“encoder{0}".format(i))) for i in xrange(output_seq_len + 1): decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], name=“decoder{0}".format(i))) for i in xrange(output_seq_len): target_weights.append(tf.placeholder(tf.float32, shape=[None], name=“weight{0}".format(i))) # decoder_inputs左移一个时序作为targets targets = [decoder_inputs[i + 1] for i in xrange(output_seq_len)] cell = tf.contrib.rnn.BasicLSTMCell(size) # 这里输出的状态我们不需要 outputs, _ = seq2seq.embedding_attention_seq2seq( encoder_inputs, decoder_inputs[:output_seq_len], cell, num_encoder_symbols=num_encoder_symbols, num_decoder_symbols=num_decoder_symbols, embedding_size=size, output_projection=None, feed_previous=False, dtype=tf.float32) # 计算加权交叉熵损失 loss = seq2seq.sequence_loss(outputs, targets, target_weights) return encoder_inputs, decoder_inputs, target_weights, outputs, loss

def main(): with tf.Session() as sess: sample_encoder_inputs, sample_decoder_inputs, sample_target_weights = get_samples() encoder_inputs, decoder_inputs, target_weights, outputs, loss = get_model() input_feed = {} for l in xrange(input_seq_len): input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l] for l in xrange(output_seq_len): input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l] input_feed[target_weights[l].name] = sample_target_weights[l] input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32) sess.run(tf.global_variables_initializer()) loss = sess.run(loss, input_feed) print loss

if name == “main": main()

我们可以运行以上代码来输出最终loss值

到这里远远没有结束,我们的旅程才刚刚开始,下面是怎么训练这个模型,首先我们需要经过多轮计算让这里的loss变得很小,这就需要运用梯度下降来更新参数,我们先来看一下tensorflow提供给我们的梯度下降的类:

https://www.tensorflow.org/api_docs/python/tf/train/GradientDescentOptimizer

Class GradientDescentOptimizer的构造方法如下:

__init__(
    learning_rate,
    use_locking=False,
    name='GradientDescent'
)

其中关键就是第一个参数:学习率

他的另外一个方法是计算梯度:

compute_gradients(
    loss,
    var_list=None,
    gate_gradients=GATE_OP
    aggregation_method=None,
    colocate_gradients_with_ops=False,
    grad_loss=None
)

其中关键参数loss就是传入的误差值,他的返回值是(gradient, variable)组成的list

再看另外一个方法是更新参数:

apply_gradients(
    grads_and_vars,
    global_step=None,
    name=None
)

其中grads_and_vars就是compute_gradients的返回值

那么根据loss计算梯度并更新参数的方法如下:

learning_rate = 0.1
opt = tf.train.GradientDescentOptimizer(learning_rate)
update = opt.apply_gradients(opt.compute_gradients(loss))

所以,我们队main函数增加个循环迭代,如下:

def main():
    with tf.Session() as sess:
        sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                          = get_samples()
        encoder_inputs, decoder_inputs, target_weights, outputs, loss, update 
                          = get_model()
        input_feed = {}
        for l in xrange(input_seq_len):
            input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
        for l in xrange(output_seq_len):
            input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
            input_feed[target_weights[l].name] = sample_target_weights[l]
        input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)
        sess.run(tf.global_variables_initializer())
        while True:
            [loss_ret, _] = sess.run([loss, update], input_feed)
            print loss_ret

运行代码后我们看到loss_ret唰唰的收敛,如下:

2.82825
2.51962
2.22479
1.92842
1.62379
1.31437
1.01518
0.749776
……
0.00601374
0.00590649
0.00580256
0.00570199
0.00560467
0.00551019
0.00541872
……

看来我们的训练逻辑可以跑通了,接下来就是实现预测的逻辑了,就是我们只输入样本的encoder_input,看能不能自动预测出decoder_input

首先,我们要能够把训练好的模型保存起来,以便重新启动做预测时能够加载:

def get_model():
      ...
saver = tf.train.Saver(tf.global_variables())
      return ..., saver

在训练结束后执行

saver.save(sess, './model/demo')

这样模型会存储到./model目录下以demo开头的一些文件中,之后我们要加载时就先调用:

saver.restore(sess, './model/demo')

其次,因为我们在做预测的时候,原则上不能有decoder_inputs输入了,所以在执行时的decoder_inputs就要取前一个时序的输出,这时候embedding_attention_seq2seq的feed_previous参数有派上用场了,这个参数的含义就是:若为True则decoder里每一步输入都用前一步的输出来填充,如下图:

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

所以,我们的get_model需要传递参数来区分训练时和预测是不同的feed_previous配置,另外,考虑到预测时main函数也是不同的,索性我们分开两个函数来分别做train和predict,整理好的一份完整代码如下(为了更好理解,完整代码和上面稍有出入,请以这份代码为准):

# coding:utf-8
import sys
import numpy as np
import tensorflow as tf
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq
# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# 结尾标记
EOS_ID = 2
# LSTM神经元size
size = 8
# 最大输入符号数
num_encoder_symbols = 10
# 最大输出符号数
num_decoder_symbols = 16
# 学习率
learning_rate = 0.1

def get_samples(): “““构造样本数据 :return: encoder_inputs: [array([0, 0], dtype=int32), array([0, 0], dtype=int32), array([5, 5], dtype=int32), array([7, 7], dtype=int32), array([9, 9], dtype=int32)] decoder_inputs: [array([1, 1], dtype=int32), array([11, 11], dtype=int32), array([13, 13], dtype=int32), array([15, 15], dtype=int32), array([2, 2], dtype=int32)] "”” train_set = [[[5, 7, 9], [11, 13, 15, EOS_ID]], [[7, 9, 11], [13, 15, 17, EOS_ID]]] raw_encoder_input = [] raw_decoder_input = [] for sample in train_set: raw_encoder_input.append([PAD_ID] * (input_seq_len - len(sample[0])) + sample[0]) raw_decoder_input.append([GO_ID] + sample[1] + [PAD_ID] * (output_seq_len - len(sample[1]) - 1)) encoder_inputs = [] decoder_inputs = [] target_weights = [] for length_idx in xrange(input_seq_len): encoder_inputs.append(np.array([encoder_input[length_idx] for encoder_input in raw_encoder_input], dtype=np.int32)) for length_idx in xrange(output_seq_len): decoder_inputs.append(np.array([decoder_input[length_idx] for decoder_input in raw_decoder_input], dtype=np.int32)) target_weights.append(np.array([ 0.0 if length_idx == output_seq_len - 1 or decoder_input[length_idx] == PAD_ID else 1.0 for decoder_input in raw_decoder_input ], dtype=np.float32)) return encoder_inputs, decoder_inputs, target_weights

def get_model(feed_previous=False): “““构造模型 "”” encoder_inputs = [] decoder_inputs = [] target_weights = [] for i in xrange(input_seq_len): encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], name=“encoder{0}".format(i))) for i in xrange(output_seq_len + 1): decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], name=“decoder{0}".format(i))) for i in xrange(output_seq_len): target_weights.append(tf.placeholder(tf.float32, shape=[None], name=“weight{0}".format(i))) # decoder_inputs左移一个时序作为targets targets = [decoder_inputs[i + 1] for i in xrange(output_seq_len)] cell = tf.contrib.rnn.BasicLSTMCell(size) # 这里输出的状态我们不需要 outputs, _ = seq2seq.embedding_attention_seq2seq( encoder_inputs, decoder_inputs[:output_seq_len], cell, num_encoder_symbols=num_encoder_symbols, num_decoder_symbols=num_decoder_symbols, embedding_size=size, output_projection=None, feed_previous=feed_previous, dtype=tf.float32) # 计算加权交叉熵损失 loss = seq2seq.sequence_loss(outputs, targets, target_weights) # 梯度下降优化器 opt = tf.train.GradientDescentOptimizer(learning_rate) # 优化目标:让loss最小化 update = opt.apply_gradients(opt.compute_gradients(loss)) # 模型持久化 saver = tf.train.Saver(tf.global_variables()) return encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver, targets

def train(): "”” 训练过程 "”” with tf.Session() as sess: sample_encoder_inputs, sample_decoder_inputs, sample_target_weights = get_samples() encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver, targets = get_model() input_feed = {} for l in xrange(input_seq_len): input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l] for l in xrange(output_seq_len): input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l] input_feed[target_weights[l].name] = sample_target_weights[l] input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32) # 全部变量初始化 sess.run(tf.global_variables_initializer()) # 训练200次迭代,每隔10次打印一次loss for step in xrange(200): [loss_ret, _] = sess.run([loss, update], input_feed) if step % 10 == 0: print ‘step=’, step, ‘loss=’, loss_ret # 模型持久化 saver.save(sess, ‘./model/demo’)

def predict(): "”” 预测过程 "”” with tf.Session() as sess: sample_encoder_inputs, sample_decoder_inputs, sample_target_weights = get_samples() encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver, targets = get_model(feed_previous=True) # 从文件恢复模型 saver.restore(sess, ‘./model/demo’) input_feed = {} for l in xrange(input_seq_len): input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l] for l in xrange(output_seq_len): input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l] input_feed[target_weights[l].name] = sample_target_weights[l] input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32) # 预测输出 outputs = sess.run(outputs, input_feed) # 一共试验样本有2个,所以分别遍历 for sample_index in xrange(2): # 因为输出数据每一个是num_decoder_symbols维的 # 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能 outputs_seq = [int(np.argmax(logit[sample_index], axis=0)) for logit in outputs] # 如果是结尾符,那么后面的语句就不输出了 if EOS_ID in outputs_seq: outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)] outputs_seq = [str(v) for v in outputs_seq] print " “.join(outputs_seq)

if name == “main": if sys.argv[1] == ‘train’: train() else: predict()

比如文件命名为demo.py,那么首先执行./demo.py train训练好模型,然后执行./demo.py predict,输出为:

11 13 15
11 13 15

至此,我们算有了小小的成就了,那就是我们根据输入的样本(两个[5, 7, 9])预测出了输出的11 13 15了

比较仔细的人会发现,在做预测的时候依然是按照完整的encoder_inputs和decoder_inputs计算的,那么怎么能证明模型不是直接使用了decoder_inputs来预测出的输出呢?那么我们来继续改进predict,让我们可以手工输入一串数字(只有encoder部分),看看模型能不能预测出输出

首先我们实现一个从输入空格分隔的数字id串,转成预测用的encoder、decoder、target_weight的函数

def seq_to_encoder(input_seq):
    """从输入空格分隔的数字id串,转成预测用的encoder、decoder、target_weight等
    """
    input_seq_array = [int(v) for v in input_seq.split()]
    encoder_input = [PAD_ID] * (input_seq_len - len(input_seq_array)) + input_seq_array
    decoder_input = [GO_ID] + [PAD_ID] * (output_seq_len - 1)
    encoder_inputs = [np.array([v], dtype=np.int32) for v in encoder_input]
    decoder_inputs = [np.array([v], dtype=np.int32) for v in decoder_input]
    target_weights = [np.array([1.0], dtype=np.float32)] * output_seq_len
    return encoder_inputs, decoder_inputs, target_weights

然后我们改写predict函数如下:

def predict():
    """
    预测过程
    """
    with tf.Session() as sess:
        encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver 
                          = get_model(feed_previous=True)
        saver.restore(sess, './model/demo')
        sys.stdout.write("> ")
        sys.stdout.flush()
        input_seq = sys.stdin.readline()
        while input_seq:
            input_seq = input_seq.strip()
            sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                          = seq_to_encoder(input_seq)
            input_feed = {}
            for l in xrange(input_seq_len):
                input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
            for l in xrange(output_seq_len):
                input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
                input_feed[target_weights[l].name] = sample_target_weights[l]
            input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)
            # 预测输出
            outputs_seq = sess.run(outputs, input_feed)
            # 因为输出数据每一个是num_decoder_symbols维的
            # 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
            outputs_seq = [int(np.argmax(logit[0], axis=0)) for logit in outputs_seq]
            # 如果是结尾符,那么后面的语句就不输出了
            if EOS_ID in outputs_seq:
                outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
            outputs_seq = [str(v) for v in outputs_seq]
            print " ".join(outputs_seq)
            sys.stdout.write("> ")
            sys.stdout.flush()
            input_seq = sys.stdin.readline()

好,重新执行./demo.py predict如下:

[root@localhost $] python demo.py predict
> 5 7 9
11 13 15
>

我们实际上只训练了两个完全一样的样本:[5, 7, 9], [11, 13, 15, EOS_ID],那么我们如果输入一个新的测试样本会怎么样呢?他能不能预测出我们是在推导奇数序列呢?

当我们输入7 9 11的时候发现他报错了,原因是我们设置了num_encoder_symbols = 10,而11无法表达了,所以我们为了训练一个强大的模型,我们修改参数并增加样本,如下:

# 最大输入符号数
num_encoder_symbols = 32
# 最大输出符号数
num_decoder_symbols = 32
……
train_set = [
              [[5, 7, 9], [11, 13, 15, EOS_ID]], 
              [[7, 9, 11], [13, 15, 17, EOS_ID]], 
              [[15, 17, 19], [21, 23, 25, EOS_ID]]
            ]
……

我们把迭代次数扩大到10000次

训练好后重新预测结果如下:

> 5 7 9
11 13 15
> 11 13 15
13 17
> 7 9 11
13 15 17
> 15 17 19
21 23 25
> 6 8 10
11 13 15
>

我们发现,对于输入的样本,预测效果还是非常好的,但是如果换成其他的输入,就还是在样本的输出里找某一个最相近的结果作为预测结果,并不会思考,没有智能,所以这个模型更适合做分类,不适合做推理

但是现在,我们依然在玩的只是数字的游戏,怎么样才能和中文对话扯上关系呢?很简单,在训练时把中文词汇转成id号,在预测时,把预测到的id转成中文就可以了

下面我们新建一个word_token.py文件,并建一个WordToken类,其中load函数负责加载样本,并生成word2id_dict和id2word_dict词典,word2id函数负责将词汇转成id,id2word负责将id转成词汇:

# coding:utf-8
import sys
import jieba

class WordToken(object): def init(self): # 最小起始id号, 保留的用于表示特殊标记 self.START_ID = 4 self.word2id_dict = {} self.id2word_dict = {}

def load_file_list(self, file_list):
    """
    加载样本文件列表,全部切词后统计词频,按词频由高到低排序后顺次编号
    并存到self.word2id_dict和self.id2word_dict中
    """
    words_count = {}
    for file in file_list:
        with open(file, 'r') as file_object:
            for line in file_object.readlines():
                line = line.strip()
                seg_list = jieba.cut(line)
                for str in seg_list:
                    if str in words_count:
                        words_count[str] = words_count[str] + 1
                    else:
                        words_count[str] = 1
    sorted_list = [[v[1], v[0]] for v in words_count.items()]
    sorted_list.sort(reverse=True)
    for index, item in enumerate(sorted_list):
        word = item[1]
        self.word2id_dict[word] = self.START_ID + index
        self.id2word_dict[self.START_ID + index] = word

def word2id(self, word):
    if not isinstance(word, unicode):
        print "Exception: error word not unicode"
        sys.exit(1)
    if word in self.word2id_dict:
        return self.word2id_dict[word]
    else:
        return None

def id2word(self, id):
    id = int(id)
    if id in self.id2word_dict:
        return self.id2word_dict[id]
    else:
        return None</code></pre>

下面在demo.py中修改我们的get_train_set如下:

def get_train_set():
    global num_encoder_symbols, num_decoder_symbols
    train_set = []
    with open('./samples/question', 'r') as question_file:
        with open('./samples/answer', 'r') as answer_file:
            while True:
                question = question_file.readline()
                answer = answer_file.readline()
                if question and answer:
                    question = question.strip()
                    answer = answer.strip()
                    question_id_list = get_id_list_from(question)
                    answer_id_list = get_id_list_from(answer)
                    answer_id_list.append(EOS_ID)
                    train_set.append([question_id_list, answer_id_list])
                else:
                    break
    return train_set

其中这里的get_id_list_from实现为:

def get_id_list_from(sentence):
    sentence_id_list = []
    seg_list = jieba.cut(sentence)
    for str in seg_list:
        id = wordToken.word2id(str)
        if id:
            sentence_id_list.append(wordToken.word2id(str))
    return sentence_id_list

而这里的wordToken来自于:

import word_token
import jieba
wordToken = word_token.WordToken()
# 放在全局的位置,为了动态算出num_encoder_symbols和num_decoder_symbols
max_token_id = wordToken.load_file_list(['./samples/question', './samples/answer'])
num_encoder_symbols = max_token_id + 5
num_decoder_symbols = max_token_id + 5

然后我们把训练代码改成:

        # 训练很多次迭代,每隔10次打印一次loss,可以看情况直接ctrl+c停止
        for step in xrange(100000):
            [loss_ret, _] = sess.run([loss, update], input_feed)
            if step % 10 == 0:
                print 'step=', step, 'loss=', loss_ret
                # 模型持久化
                saver.save(sess, './model/demo')

预测的代码我们也需要做相应的修改:

def predict():
    """
    预测过程
    """
    with tf.Session() as sess:
        encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver 
                        = get_model(feed_previous=True)
        saver.restore(sess, './model/demo')
        sys.stdout.write("> ")
        sys.stdout.flush()
        input_seq = sys.stdin.readline()
        while input_seq:
            input_seq = input_seq.strip()
            input_id_list = get_id_list_from(input_seq)
            if (len(input_id_list)):
                sample_encoder_inputs, sample_decoder_inputs, sample_target_weights 
                        = seq_to_encoder(' '.join([str(v) for v in input_id_list]))
                input_feed = {}
                for l in xrange(input_seq_len):
                    input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
                for l in xrange(output_seq_len):
                    input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
                    input_feed[target_weights[l].name] = sample_target_weights[l]
                input_feed[decoder_inputs[output_seq_len].name] 
                        = np.zeros([2], dtype=np.int32)
                # 预测输出
                outputs_seq = sess.run(outputs, input_feed)
                # 因为输出数据每一个是num_decoder_symbols维的
                # 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
                outputs_seq = [int(np.argmax(logit[0], axis=0)) for logit in outputs_seq]
                # 如果是结尾符,那么后面的语句就不输出了
                if EOS_ID in outputs_seq:
                    outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
                outputs_seq = [wordToken.id2word(v) for v in outputs_seq]
                print " ".join(outputs_seq)
            else:
                print "WARN:词汇不在服务区"
            sys.stdout.write("> ")
            sys.stdout.flush()
            input_seq = sys.stdin.readline()

下面我们尝试用存储在['./samples/question', './samples/answer']中的1000个对话样本来训练,使得loss输出收敛到一定程度(比如1.0)以下:

python demo.py train

到1.0以下后可以手工ctrl+c停止,因为我们每隔10个step都会store一次模型

训练情况如下:

[root@localhost $] python demo.py train
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/tq/c0kp5y857x138x5vf8bzxfc80000gp/T/jieba.cache
Loading model cost 0.442 seconds.
Prefix dict has been built succesfully.
step= 0 loss= 7.57641
step= 10 loss= 7.51317
step= 20 loss= 7.4462
step= 30 loss= 7.37086
step= 40 loss= 7.28131
step= 50 loss= 7.1697
step= 60 loss= 7.02524
step= 70 loss= 6.83556
step= 80 loss= 6.60554
step= 90 loss= 6.41139

我们发现模型收敛的非常慢,因为我们设置的学习率是0.1,我们希望首先学习率大一些,每当下一步的loss和上一步相比反弹(反而增大)的时候我们再尝试降低学习率,方法如下,首先我们不再直接用learning_rate,而是初始化一个学习率:

init_learning_rate = 1

然后在get_model中创建一个变量,并用init_learning_rate初始化:

learning_rate = tf.Variable(float(init_learning_rate), trainable=False, dtype=tf.float32)

之后再创建一个操作,目的是再适当的时候把学习率打9折:

learning_rate_decay_op = learning_rate.assign(learning_rate * 0.9)

之后在训练代码中这样调整:

        # 训练很多次迭代,每隔10次打印一次loss,可以看情况直接ctrl+c停止
        previous_losses = []
        for step in xrange(100000):
            [loss_ret, _] = sess.run([loss, update], input_feed)
            if step % 10 == 0:
                print 'step=', step, 'loss=', 
                        loss_ret, 'learning_rate=', learning_rate.eval()
                if loss_ret > max(previous_losses[-5:]):
                    sess.run(learning_rate_decay_op)
                previous_losses.append(loss_ret)
                # 模型持久化
                saver.save(sess, './model/demo')

这样在训练时可以实现快速收敛:

[root@localhost $] python demo.py train
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/tq/c0kp5y857x138x5vf8bzxfc80000gp/T/jieba.cache
Loading model cost 0.465 seconds.
Prefix dict has been built succesfully.
step= 0 loss= 7.5769 learning_rate= 1.0
step= 10 loss= 6.32053 learning_rate= 1.0
step= 20 loss= 5.73492 learning_rate= 1.0
step= 30 loss= 5.59705 learning_rate= 1.0
step= 40 loss= 5.5177 learning_rate= 1.0
step= 50 loss= 5.47604 learning_rate= 1.0
step= 60 loss= 5.39912 learning_rate= 1.0
step= 70 loss= 5.29099 learning_rate= 1.0
step= 80 loss= 5.60937 learning_rate= 1.0
step= 90 loss= 5.10457 learning_rate= 0.9
step= 100 loss= 5.07913 learning_rate= 0.9
step= 110 loss= 5.03715 learning_rate= 0.9
step= 120 loss= 5.14339 learning_rate= 0.9
step= 130 loss= 5.29311 learning_rate= 0.9
step= 140 loss= 5.04177 learning_rate= 0.9
step= 150 loss= 4.98884 learning_rate= 0.9

在我的mac机器历时10个小时,终于修成正果,一共1000条样本,共进行了20700次迭代,最终loss收敛到1.11342:

step= 20700 loss= 1.11342 learning_rate= 0.0471012

预测效果如下:

[root@localhost $] python demo.py predict
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/tq/c0kp5y857x138x5vf8bzxfc80000gp/T/jieba.cache
Loading model cost 0.969 seconds.
Prefix dict has been built succesfully.
> 你好
你 也好 ~
> 呵呵
傻 逼 呵呵
> 哈哈
笑 屁
> 你是谁
我 是 小猴子
> 早
WARN:词汇不在服务区
> 早上好
哈哈
> 屁
你 屁 会
> 滚蛋
WARN:词汇不在服务区
> 傻逼
他妈 逼 的

看起来效果还是有点靠谱的,最终版代码分享在:https://github.com/lcdevelop/ChatBotCourse/tree/master/chatbotv5

 

总结

本文从理论到实践讲解了怎么一步一步实现一个自动聊天机器人模型,并基于1000条样本,用了10个小时训练了一个聊天模型,试验效果比较好,核心逻辑是调用了tensorflow的embedding_attention_seq2seq,也就是带注意力的seq2seq模型,其中神经网络单元是LSTM。

由于语料有限,设备有限,只验证了小规模样本,如果你希望训练一个强大的聊天机器人可以自己来搞到高质量的对话语料,也欢迎试用我整理的3千万影视剧字幕语料库,获取方式:《自己动手做聊天机器人 二十九-重磅:近1GB的三千万聊天语料供出》

更欢迎聊天机器人爱好者加入我们的“自己动手做聊天机器人微信交流群”,加群方式:扫描下方二维码加小二兔为好友,并发送“加群”,即可收到入群邀请

 

参考文献

http://colah.github.io/posts/2015-08-Understanding-LSTMs/

http://suriyadeepan.github.io/2016-06-28-easy-seq2seq/

http://www.wildml.com/2016/01/attention-and-memory-in-deep-learning-and-nlp/

https://arxiv.org/abs/1406.1078

https://arxiv.org/abs/1409.3215

https://arxiv.org/abs/1409.0473

从0到1搭建个人网站 九-实现真实用户的pv计算

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

pv计算如何过滤爬虫

首先我们要理解爬虫是干什么的,爬虫是抓取你网页的内容,之后用于自己的搜索引擎或者做数据挖掘或者盗用,因此爬虫想要的其实是你的网页里的文本内容,也可能需要样式、布局等,但因为抓取效率的考虑,不会渲染里面的js,也不会抓取里面的图片链接,所以利用这一点,我们找到一个方法可以很好的过滤爬虫的干扰

 

创建js文件动态生成gif图片

创建web/static/web/my/pv.js,内容如下:

// 为了过滤爬虫,上报的信息是通过js渲染进行的,同时渲染出来的是个图片,这样可以过滤大部分爬虫,同时异步加载和更新pv,不影响用户体验
$(function()
{
    var gif_url = '/report/pv.gif?url=' + window.location.href;
	var report ="<img id='report_gif' src='" + gif_url + "'>";
	$("#report").html(report);
    $("#report").css('height', 0);
    $("#report").css('width', 0);
});

 

把这个js添加到所有网页公共的模板的head标签中,即base.html模板

<script src="{% static '/web/my/pv.js' %}"></script>

并在base.html的body中增加如下节点,这样js里动态生成gif就会放到这里:

<div id="report"></div>

 

下面我们创建一个1像素大小的gif文件,并放到web/static/web/images/onepixel.gif路径下

 

为了响应这个动态gif的浏览器请求,我们实现如下方法:

在web/views.py中添加:

import os
import re
from django.conf import settings
report_url_pattern = re.compile(r'.*blogId=(\d+).*')
def report_pv(request):
    if 'url' in request.GET:
        url = request.GET['url']
        m = re.match(report_url_pattern, url)
        if m:
            blog_id = int(m.group(1))
            blog = BlogPost.objects.get(id=blog_id)
            blog.pv = blog.pv + 1
            blog.save()
    image_data = open(os.path.join(settings.BASE_DIR, 'web/static/web/images/onepixel.gif'), "rb").read()
    return HttpResponse(image_data, content_type="image/gif")

 

在urls.py添加

url(r'^report/pv.gif$', views.report_pv, name='report_pv'),

 

好了,现在大功告成,这样我们通过浏览器访问可以正常统计到pv,而爬虫过来就不会统计上去了

从0到1搭建个人网站 八-django中集成ckeditor

安装ckeditor

django集成ckeditor已经有人帮我们做了,我们直接参考https://github.com/django-ckeditor/django-ckeditor安装,方法如下:

pip install django-ckeditor

然后修改shareditor/settings.py中的INSTALLED_APPS,添加如下项:

    'ckeditor',
    'ckeditor_uploader',

再在shareditor/settings.py中添加:

STATIC_ROOT = os.path.join(BASE_DIR, "static/")
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
CKEDITOR_UPLOAD_PATH = 'ckeditor_images'
CKEDITOR_CONFIGS = {
    'default': {
        'skin': 'moono',
        'toolbar_YourCustomToolbarConfig': [
            {'name': 'basicstyles',
             'items': ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript']},
            {'name': 'links', 'items': ['Link', 'Unlink']},
            {'name': 'insert',
             'items': ['CodeSnippet', 'Image', 'Table', 'PageBreak']},
            '/',
            {'name': 'styles', 'items': ['Styles', 'Format', 'Font', 'FontSize']},
        ],
        'toolbar': 'YourCustomToolbarConfig',  # put selected toolbar config here
        'tabSpaces': 4,
        'extraPlugins': ','.join([
            'uploadimage', # the upload image feature
            'div',
            'autolink',
            'autoembed',
            'embedsemantic',
            'autogrow',
            'widget',
            'dialog',
            'lineutils',
            'codesnippet'
        ]),
        'codeSnippet_theme': 'atelier-dune.dark',
    }
}

修改urls.py,改为:

from django.conf.urls import url, include
from django.conf.urls.static import static
from django.conf import settings
......
    url(r'^ckeditor/', include('ckeditor_uploader.urls'))
 ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

 

然后执行如下命令来拷贝静态文件:

./manage.py collectstatic

 

为了在管理界面能展示ckeditor,我们将model中的富文本字段改成:

from ckeditor_uploader.fields import RichTextUploadingField
body = RichTextUploadingField(config_name='default', verbose_name='文章内容')

(这里的default要和settings.py文件里的CKEDITOR_CONFIGS对应)

 

然后在文章的展示模板里要添加:

{% load static %}
{% block head %}
    <link href="{% static 'ckeditor/ckeditor/plugins/codesnippet/lib/highlight/styles/atelier-dune.dark.css' %}" rel="stylesheet">
    <script src="{% static 'ckeditor/ckeditor/plugins/codesnippet/lib/highlight/highlight.pack.js' %}"></script>
    <script>hljs.initHighlightingOnLoad();</script>
{% endblock %}

 

效果展示

管理界面如下:

 

博客展示页面如下:

 

文件上传对接阿里oss

考虑到我们的服务需要部署在多台机器,图片静态文件上传后还要共享到每台服务器的方案不是很合理,而且没有cdn支持,所以我们自定义文件上传接口来实现文件上传到阿里云的oss对象存储服务

修改web/views.py,实现自定义的文件上传方法如下:

from commons.ossutils import upload_oss
BucketName = 'shareditor-shareditor'
def body_upload(request):
    print request.FILES
    if 'upload' in request.FILES:
        image_name = request.FILES['upload'].name
        image_content = request.FILES['upload'].read()
        url = upload_oss(BucketName, image_name, image_content)
        if url:
            return render(request, 'web/body_upload.html', {'url': url})
    return HttpResponse('upload fail')

这里的web/body_upload.html模板实现如下:

<script type='text/javascript'>
    window.parent.CKEDITOR.tools.callFunction(1, "{{ url }}");
</script>

在urls.py中增加路由:

url(r'^uploader/body_upload', views.body_upload, name='body_upload'),

修改ckeditor的配置,增加:

'filebrowserUploadUrl': '/uploader/body_upload',

现在我们在ckeditor中上传图片,就会自动上传到阿里云oss啦

从0到1搭建个人网站 七-在django模板中使用自定义属性

展示简单的文章标题

首先我们来把这样的标题《教你成为全栈工程师(Full Stack Developer) 十九-文章内容展示页面设计》去掉前面tag部分变成《十九-文章内容展示页面设计》,直接在模板里操作会比较复杂,这时我们来自定义一个get_simple_title的属性,修改web/models.py,为BlogPost类添加如下方法

def get_simple_title(self):
    return self.title.replace(self.tags.first().name, '')

这时我们修改web/templates/web/blog_show.html如下:

{% for blog_post in tag.get_latest_blogpost.all %}
    {% if forloop.counter < 5 %}
        <a href="{% url 'blog_show' %}?blogId={{ blog_post.id }}">
            {{ blog_post.get_simple_title }}
        </a>
        <br />
    {% endif %}
{% endfor %}

这样便直接展示了去掉标签后的标题了

 

获取最新n篇文章

在首页分标签栏目中,我们要展示最新发布的5篇文章,这同样难以在模板中实现,因此我们自定义get_latest_blogpost,在web/models.py的Tag类中添加:

def get_latest_blogpost(self, count=5):
    return self.blogpost_set.order_by('id').reverse()[0:count]

使用方法同上面文章标题用法

 

总结

本节简答介绍了自定义属性的使用场景和方法,这是在模板中遇到困难时第一个想到的方法,简便易行,下一节我们来介绍丰富的编辑器ckeditor的集成

从0到1搭建个人网站 六-路由的使用

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

指定标签的文章列表页

上一节完成的首页部分每个标签有对应的一块展示区域,我们希望点击标题可以进入到这个标签的文章列表页。我们来定义如下路由规则,修改shareditor/urls.py,为urlpatterns增加如下一行:

url(r'^bloglistbytag', views.blog_list_by_tag, name='blog_list_by_tag')

这个意思是说对于url路径为bloglistbytag的网页,直接调用views.blog_list_by_tag来执行逻辑。其中的name是用来在模板中利用“url”模板语法使用的,马上会看到

在web/views.py中添加如下函数:

def blog_list_by_tag(request):
    if 'tagname' in request.GET:
        tag_name = request.GET['tagname']
        blog_posts = BlogPost.objects.filter(tags__name=tag_name)
        latest_blog_posts = BlogPost.objects.order_by('create_time')[0:5]
        return render(request, 'web/blog_list_by_tag.html', {'tag_name': tag_name, 'blog_posts': blog_posts,
                                                             'latest_blog_posts': latest_blog_posts})
    else:
        return HttpResponse('404')

这里首先通过获取GET请求的tagname参数来获取到标签名,然后通过model层查询数据库获取导数据,并通过web/templates/web/blog_list_by_tag.html这个模板来渲染的,这个模板如下样子:

{% extends "web/base.html" %}
{% block title %}
    {{ tag_name }}
{% endblock %}
{% block body %}
    <div class="row" style="margin-right: 0">
        <div class="col-sm-3 col-xs-1"></div>
        <div class="col-sm-6 col-xs-10">
            <h1>{{ tag_name }}</h1>
        </div>
        <div class="col-sm-3 col-xs-1"></div>
    </div>
    <div class="row" style="margin-right: 0">
        <div class="col-sm-3 col-xs-1"></div>
        <div class="col-sm-6 col-xs-10">
            {% for blog_post in blog_posts %}
                <h4><a href="">{{ blog_post }}</a>({{ blog_post.create_time|date:'Y-m-d' }})</h4>
            {% endfor %}
        </div>
    </div>
{% endblock %}

以上我们定义了一个标签列表页,打开这个url可以看到效果:http://127.0.0.1:8000/bloglistbytag?tagname=%E4%BB%8E0%E5%88%B01%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99

 

但是我们怎么通过首页链接进到这个列表页呢?我们修改web/templates/web/index.html,把其中的

<h3>{{ tag }}({{ tag.blogpost_set.count }})</h3>

改成

<h3><a href="{% url 'blog_list_by_tag' %}?tagname={{ tag }}">{{ tag }}({{ tag.blogpost_set.count }})</a></h3>

这里面我们见识到了url模板语法,他根据指定的路由名来去urls.py中寻找对应的逻辑函数并执行,同时通过url参数来传递变量

 

文章详情页的路由

如上面同样的方法,我们自己设计自己的文章详情页,我自己的详情页模板可以直接参考https://github.com/lcdevelop/shareditor/blob/master/web/templates/web/blog_show.html

详情页的逻辑代码如下:

def blog_show(request):
    if 'blogId' in request.GET:
        blog_id = request.GET['blogId']
        blog_post = BlogPost.objects.get(id=blog_id)
        latest_blog_posts = BlogPost.objects.order_by('create_time')[0:5]
        tag_blog_posts = BlogPost.objects.filter(tags__name=blog_post.tags.first())
        tags = Tag.objects.all()
        prev_blog_post = BlogPost.objects.filter(tags__name=blog_post.tags.first()).filter(id__gt=blog_post.id).order_by('id')[0:1]
        next_blog_post = BlogPost.objects.filter(tags__name=blog_post.tags.first()).filter(id__gt=blog_post.id).order_by('id').reverse()[0:1]
        return render(request, 'web/blog_show.html', {'blog_post': blog_post, 'latest_blog_posts': latest_blog_posts,
                                                      'tag_blog_posts': tag_blog_posts, 'tags': tags,
                                                      'prev_blog_post': prev_blog_post, 'next_blog_post': next_blog_post})
    else:
        return HttpResponse('404')

那么,需要添加链接到详情页的地方有很多处,比如首页中改成这样:

 

<p>
    {% for blog_post in tag.blogpost_set.all %}
        <a href="{% url 'blog_show' %}?blogId={{ blog_post.id }}">
            {{ blog_post.title }}
        </a>
    {% endfor %}
</p>

再比如标签列表页改成这样:

<h4><a href="{% url 'blog_show' %}?blogId={{ blog_post.id }}">{{ blog_post }}</a>({{ blog_post.create_time|date:'Y-m-d' }})</h4>

 

小技巧

详情页中我们希望增加“前一篇”和“后一篇”按钮,那么怎么快速获取前一篇和 后一篇呢?如下:

prev_blog_post = BlogPost.objects.filter(tags__name=blog_post.tags.first()).filter(id__lt=blog_post.id).order_by('id').reverse()[0:1]
next_blog_post = BlogPost.objects.filter(tags__name=blog_post.tags.first()).filter(id__gt=blog_post.id).order_by('id')[0:1]

 

总结

这一节主要介绍路由相关内容,其余跟具体网页详细内容需要根据你自己的情况自己定制,如果想参考shareditor可以直接阅读https://github.com/lcdevelop/shareditor

 

从0到1搭建个人网站 五-开启网站首页的大门

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

模板渲染

模板属于mvc中的view这一层,是用来为网页布局的,乍一看来就是个html页面,但是里面会嵌入模板引擎提供给我们的特殊语法。模板引擎有很多,不同语言或框架都会实现自己一套,还有一些通用的模板引擎,但是他们语法和功能都比较类似,无非就是模板继承、模板引入、读透传的变量、循环、逻辑判断、filters、自定义标签等。我们这里直接用django原生的django-template就足够了。

创建web/templates/web/index.html,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello {{ name }}</h1>
</body>
</html>

修改web/views.py中的index函数如下:

def index(request):
    return render(request, 'web/index.html', {'name': 'lichuang'})

打开网页我们看到了一个大大的Hello lichuang

这里的{{}}就是变量透传的语法,直接获取到python传进去的变量。另外这个模板文件的路径用的是web/index.html而不是直接index.html,原因是django里多个app如果用相同的模板文件名,在python代码里指定模板名时它分辨不出是哪个,所以一般把app的名字作为模板文件的目录

 

模板继承

我们网站的所有页面都有一个共同的布局,那就是顶部是logo,底部是版权声明,所以这个框架可以抽象出来一个基类模板,而所有页面模板都继承自这一模板,下面我们创建这个基类模板web/templates/web/base.html,内容如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
    <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css">
    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
    <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
    <title>{% block title %}lcsays - 关注大数据技术{% endblock title %}</title>
    {% block head %}
    {% endblock head %}
</head>
<body>
<div class="row bg-primary" style="margin-right: 0">
    <div class="col-sm-1 col-xs-1"></div>
    <div class="col-sm-2 col-xs-11">
        <h1><a href="" style="text-decoration: none;color: white;">lcsays</a></h1>
    </div>
    <div class="col-sm-6"></div>
</div>
{% block body %}
{% endblock body %}
<div class="row" style="margin-right: 0">
    <div class="col-sm-2"></div>
    <div class="col-sm-8 col-xs-12 text-center" style="color: #959595;margin-bottom: 10px;">
        Copyright © <a href="">shareditor.com</a> | 京ICP备13047771号 | shareditor.com^_^gmail.com
    </div>
</div>
</body>
</html>

这里面有一些叫做block的代码块,它的作用是声明可重写的部分,下面你就会看到这个代码块是怎么在子模板中使用的。下面我们重新修改web/templates/web/index.html:

{% extends "web/base.html" %}
{% block title %}
{% endblock %}
{% block body %}
{% endblock %}

这里我们只是做了继承,并没有重写,打开网页看到如下的效果:

下面我们来完善首页的部分

在{% block body %}块

中添加如下代码:

    <div class="row jumbotron" style="margin-right: 0">
        <div class="col-md-1 col-xs-1"></div>
        <div class="col-md-10 col-xs-10"><h1>Welcome Big Data ITors!</h1></div>
        <div class="col-md-1 col-xs-1"></div>
    </div>

重新打开网页看到如下效果:

 

动态数据展示

下面我们来从数据获取tag数据,并展示到首页里。修改web/views.py的index函数,把tag数据取出来并传到模板中,如下:

def index(request):
    tags = Tag.objects.all()
    return render(request, 'web/index.html', {'tags': tags})

 

修改web/templates/web/index.html:

{% extends "web/base.html" %}
{% block title %}
{% endblock %}
{% block body %}
    <div class="row jumbotron" style="margin-right: 0">
        <div class="col-md-1 col-xs-1"></div>
        <div class="col-md-10 col-xs-10"><h1>Welcome Big Data ITors!</h1></div>
        <div class="col-md-1 col-xs-1"></div>
    </div>
    <div class="row" style="margin-right: 0">
        <div class="col-sm-1 col-xs-1"></div>
        {% for tag in tags %}
            <div class="col-sm-2 col-xs-12">
                <div class="thumbnail">
                    <img src="{{ tag.image }}" alt="tag">
                    <div class="caption">
                        <h3>{{ tag }}({{ tag.blogpost_set.count }})</h3>
                        <p>
                            {% for blogpost in tag.blogpost_set.all %}
                                {{ blogpost.title }}
                            {% endfor %}
                        </p>
                        <h5>更多>>></h5>
                    </div>
                </div>
            </div>
        {% endfor %}
        <div class="col-sm-1 col-xs-1"></div>
    </div>
{% endblock %}

看下效果如何了:

渐渐高大上了有木有

不过在这里其实有一些步骤我省略了,需要你自己去发觉,比如说为Tag添加image属性

 

展示最新文章

为了用户能找到最新的文章(其实是为了SEO),我们在所有页面的底部显示最新发表的文章,这需要我们修改基类模板了,首先在views.py中透传latest_blog_posts变量,如下:

def index(request):
    tags = Tag.objects.all()
    latest_blog_posts = BlogPost.objects.order_by('create_time')[0:5]
    return render(request, 'web/index.html', {'tags': tags, 'latest_blog_posts': latest_blog_posts})

然后修改web/templates/web/base.html,在body中添加如下代码:

<div class="row navbar navbar-inverse" style="margin:0">
    <div class="row" style="margin:0">
        <div class="col-sm-1 col-xs-1"></div>
        <div class="col-sm-5 col-xs-5">
            <h4 style="color: #FFFFFF; border-bottom: 1px solid #695d69; padding-bottom: 10px; margin-top: 30px;">最新文章</h4>
            {% for blogpost in latest_blog_posts %}
                <div class="row" style="margin: 10px; margin-left: 0; overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
                    <span style="color: #959595;">({{ blogpost.create_time|date:"Y-m-d" }})</span>
                    <a title="{{ blogpost }}" style="color: #959595;">{{ blogpost }}</a>
                </div>
            {% endfor %}
        </div>
        <div class="col-sm-1 col-xs-1"></div>
    </div>
</div>

效果如下:

现在我们的首页初见雏形了,剩下的就是充实数据啦。今天到此为止,下一节我们继续完善其他页面。

从0到1搭建个人网站 四-强大的后台管理工具django-admin

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

django-admin的账户管理

当我们直接打开http://127.0.0.1:8000/admin时,虽然能够看到管理后台登陆界面,但是我们没有账号密码是无法登陆的,需要我们初始化一个超级用户,那么方法如何呢?我们可以通过执行python manager.py输出的提示来找到createsuperuser这个命令,执行:

python manage.py createsuperuser

按照提示输入账号、邮箱、密码,然后再次进入http://127.0.0.1:8000/admin就可以登陆了

登陆进入后我们看到了Group和Users两个管理项,这实际上对应着数据库里的auth_group和auth_user表,在Users里能看到我们刚刚创建的超级用户,在这里我们可以添加新的用户,并为不同用户配置不同权限

 

配置admin管理数据库

还记得上一节我们创建的三个Model及其数据库表吗?BlogPost、Subject、Tag,那么怎么才能在django-admin管理后台管理这三个表的内容呢?

修改web/admin.py,添加如下内容:

from django.contrib import admin
from .models import BlogPost
class BlogPostAdmin(admin.ModelAdmin):
    list_display = ('title', 'create_time', 'subject', 'tags')
admin.site.register(BlogPost, BlogPostAdmin)

重新登陆http://127.0.0.1:8000/admin管理页面,我们看到:

点进去可以看到文章的管理页面,因为我们在BlogPost这个model里设置的subject和tags字段不可以为空,因此我们需要提前添加类别和标签,下面我们再完善一下Subject和Tags的管理类,如下:

class SubjectAdmin(admin.ModelAdmin):
    list_display = ('name',)
class TagAdmin(admin.ModelAdmin):
    list_display = ('name',)
admin.site.register(Subject, SubjectAdmin)
admin.site.register(Tag, TagAdmin)

这时我们再尝试在管理页面新建一个Subject、一个Tag、一个BlogPost吧,建好之后可以在后台数据库直接查看到数据已经写到了数据库中

 

管理界面的定制化

爱美的同学可能会发现后台管理界面还不够漂亮和友好,比如页面顶部写的“Django administrator”能不能换成“lcsays管理后台”,比如管理首页里的“WEB”能不能改成“网站”,“Blog posts”能不能改成“文章”,另外我们在新建BlogPost的时候,类别和标签这两项里写的是“Subject object”和"Tag object",都不知道具体信息,下面我们来各个击破做一下定制

首先我们修改web/models.py,为Subject类添加如下成员:

class Meta:
    verbose_name_plural = '类别'
def __unicode__(self):
    return self.name

再为Tag类添加如下成员:

class Meta:
    verbose_name_plural = '标签'
def __unicode__(self):
    return self.name

再为BlogPost类添加如下成员:

class Meta:
    verbose_name_plural = '文章'
def __unicode__(self):
    return self.title

解释一下,这里的verbose_name_plural就是在这个结构在管理页面里的展示名称,__unicode__就是这个结构里每一个对象的展示形式,不用多说,直接看一下你的管理页面的效果就知道了

管理页面总标题因为是django-admin自身的内容,因此做定制有些复杂些,此处新手可以略过

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

在根目录下创建如下目录templates/admin,并新建base_site.html文件,内容如下:

{% extends "admin/base.html" %}
{% load i18n %}
{% block title %}{{ title }} | {% trans 'lcsays后台管理' %}{% endblock %}
{% block branding %}
<h1 id="site-name">{% trans 'lcsays后台管理' %}</h1>
{% endblock %}
{% block nav-global %}{% endblock %}

修改shareditor/settings.py文件,在TEMPLATES  =>  DIRS配置项中添加'templates',这样就可以自动找到模板目录了,实际上这里的admin/base_site.html是重写了django-admin的源码。重新打开后台管理界面看下效果吧

 

图片管理(高级内容,新手略过)

我们在新建一个类别的时候要为image字段选择一张图片,我们看到图片实际上上传到了根目录下。这种方式存在一些问题:1)如果要在网站中展示这张图片需要为其单独指定路由;2)如果网站多机部署无法实时同步数据;3)如果图片很大,会耗费很多带宽,响应慢

为了解决如上问题,我们引入阿里云的对象存储OSS服务(收费,但不贵,这里不是帮阿里云打广告,但是阿里云确实做的比较好),它的优点是有CDN加速,也就是不同地域都有镜像,访问快,而且价格低廉,可比同样的网络带宽便宜多了

OSS的使用请见官方文档,我这里直接贴代码,懂的可以参考,不懂的可以直接用

首先要在阿里云的OSS中创建一个Bucket,如shareditor-shareditor,读写权限一定要选择“公共读”

其次要安装oss2库,执行:

pip install oss2

然后在我们代码的根目录创建commons目录(用于放置所有公共组件),并在其中创建一个空的__init__.py(作为lib的目录都要有这个文件,否则无法import),并创建ossutils.py文件,内容如下:

# -*- coding: utf-8 -*-
import oss2
import time
AccessKeyId = '你的AccessKey'
AccessKeySecret = '你的AccessKey密码'
Endpoint = 'oss-cn-beijing.aliyuncs.com'
InternalEndpoint = 'oss-cn-beijing-internal.aliyuncs.com'

def upload_oss(bucket_name, file_name, bytes_content): """ :param bucket_suffix: 区分测试环境和线上环境 :param file_name: 会自动添加时间戳 :param bytes_content: 二进制的文件内容 :return: 外网可以访问的url """ auth = oss2.Auth(AccessKeyId, AccessKeySecret) bucket = oss2.Bucket(auth, Endpoint, bucket_name) file_path = ‘dynamic/’ + str(int(time.time())) + ‘_’ + file_name result = bucket.put_object(file_path, bytes_content) if result.status == 200: return ‘http://’ + bucket_name + ‘.oss-cn-beijing.aliyuncs.com/’ + file_path else: return None

下面我们重载Subject的image的上传逻辑,修改web/admin.py,引入ossuitls:

from commons.ossutils import upload_oss

声明BucketName变量下面会用到:

BucketName = 'shareditor-shareditor'

修改SubjectAdmin类,添加如下方法:

def save_model(self, request, obj, form, change):
    if 'image' in request.FILES:
        image_name = request.FILES['image'].name
        image_content = request.FILES['image'].read()
        url = upload_oss(BucketName, image_name, image_content)
        if url:
            obj.image = url
    super(SubjectAdmin, self).save_model(request, obj, form, change)

这时我们重新修改一个类目,重新上传图片,我们发现图片已经不再保存到本地文件了,而在阿里云的OSS里找到了上传的文件,而在我们的数据库里存储了这个图片在阿里云OSS中的url,可以直接访问

 

总结

有关admin管理后台的内容以上这些基本够用了,剩下的就是根据你的业务逻辑去设计自己的表结构,发挥自身的主动性啦

从0到1搭建个人网站 三-数据库与model

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

创建和链接数据库

首先,我们需要有一个mysql的服务,你可以选择自己安装启动一个mysql服务,我选择了阿里云的云数据库RDS(和本地搭建没有区别,收费但不贵),我创建了一个数据库名叫db_shareditor(如果本地搭建的mysql,执行命令是create database db_shareditor)

下面我们配置我们的网站工程来连接这个数据库,修改shareditor/settings.py里的DATABASE改成如下:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'db_shareditor',
        'USER': 'username',
        'PASSWORD': 'password',
        'HOST': 'hostip',
        'PORT': '3306',
        'OPTIONS': {
            'sql_mode': 'traditional',
        }
    }
}

这里的USER、PASSWORD、HOST、PORT都配置成你自己的,然后我们来检测一下数据库配置是否正确

[lichuang@localhost:~/Developer/shareditor $] python manage.py check
System check identified no issues (0 silenced).

看到这样的信息说明我们的配置没有问题,否则会抛出异常,这时我们根据异常信息再追查问题

 

创建model并自动生成数据库表

这里解释一下什么是model,model就是数据库表在内存里的数据结构,比如某个数据库表有A和B两个字段,那么它对应的model一般也会写成A和B两个成员,这样我们在代码里操作model的实例就是在操作数据库

每个model的设计都需要精心打磨,我们首先创建一个BlogPost的model,修改web/models.py,增加如下类定义:

class BlogPost(models.Model):
    title = models.CharField(max_length=255, verbose_name='文章标题')
    body = models.TextField(verbose_name='文章内容')
    create_time = models.DateTimeField(verbose_name='创建时间')

这个类实际上定义了一个数据库的结构,下面我们用django工具来自动根据这个结构定义生成对应的数据库表,执行:

python manage.py migrate

这时我们再看一下数据库多出了这些数据库表

auth_group
auth_group_permissions
auth_permission
auth_user
auth_user_groups
auth_user_user_permission
django_admin_log
django_content_type
django_migrations
django_session

这里比较奇怪的是怎么多出了这么一批数据而没有找到我们的blogpost呢?这是因为settings.py里的INSTALLED_APPS默认安装了一些其他的玩意,而并没有安装我们的web这款app,好,那现在我们暂时先保留默认安装的app(以后有用),把我们的web添加进去,如下:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes'
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'web',
]

现在我们重新生成migrate文件并创建数据库表,执行:

python manage.py makemigrations
python manage.py migrate

这时再看数据库表多出了web_blogpost(django会自动用app名字小写加下划线加model名的小写来作为数据库表的名字,实际上数据库表名我们也是可以自己来配置的,感兴趣自己google一下吧)

 

创建有关联关系的数据库表

我们的每篇文章都会有一个类别(subject,如大数据、全栈技术等),每个类别会对应多篇文章,也就是1对多的关系,那么我们可以利用django的models里的外键类型来关联,如下:

class Subject(models.Model):
    name = models.CharField(max_length=255, verbose_name='类别名称')
    introduce = models.CharField(max_length=255, verbose_name='类别简介')
    image = models.ImageField(verbose_name='类别图片')
class BlogPost(models.Model):
    title = models.CharField(max_length=255, verbose_name='文章标题')
    body = models.TextField(verbose_name='文章内容')
    create_time = models.DateTimeField(verbose_name='创建时间')
    subject = models.ForeignKey(Subject, verbose_name='类别', null=True)

我们重新执行:

python manage.py makemigrations
python manage.py migrate

这时再看数据库表多出了web_subject,同时web_blogpost也自动多出了一个subject_id字段

有人问了,这用不用外键有什么关系呢,手工写好一个subject_id字段,然后在代码逻辑里就把这个字段作为查询subject表的key不就行了吗?看来该是介绍IDE的时候了,我来给大家介绍一款棒棒的开发工具PyCharm,具体安装方法自己去百度,我现在装的是PyCharm 2017.1.4版本,记得一定要配置好Project Interpreter为系统里安装好django的那个python环境

下面见证奇迹的时刻到了,用PyCharm打开我们上面的shareditor工程,并打开views.py文件,我们来编辑如下一段代码:

我的天啊!好强大有木有!当我们输入几个字母前缀的时候,它会把我们用外键关联的类的各种方法都给我们列出来,再也不用苦逼的查文档了

 

创建多对多关系的数据库表

我们的每篇文章都会有多个标签(tag, 如:从0到1搭建个人网站、自己动手做聊天机器人等),每个标签会对应多篇文章,也就是多对多的关系,那么我们可以利用django的models里的ManyToMany类型来关联,我们添加Tag类如下:

class Tag(models.Model):
    name = models.CharField(max_length=255, verbose_name='标签名称')

并为BlogPost类添加如下成员:

tags = models.ManyToManyField(Tag, verbose_name='标签')

我们重新执行:

python manage.py makemigrations
python manage.py migrate

这时再看数据库表多出了web_tag和web_blogpost_tag两个表,这里的web_blogpost_tag实际上是一个关系表,也就是说,BlogPost类多了tags成员,但web_blogpost表里并没有多任何字段,但当我们在PyCharm中输入tags前缀的时候依然会看到相关提示

 

总结

这一节我们介绍了数据库表和model之间的关系,以及一对多、多对多关系的使用,下一节我们来继续讨论利用model对数据库做读写

从0到1搭建个人网站 二-轻松几步完成基本网站框架搭建

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

安装开发和运行的基本环境

首先,python是必须的,我们选择python2.7,没有安装可以根据不同的操作系统安装,如果是rhel或centos可以用yum  install python,如果是ubuntu可以用apt-get install python,如果是mac可以用brew install python,如果以上都不行可以直接下官方包安装(https://www.python.org/downloads/)

然后,安装django相关组件(当前最新版是1.11):

pip install django

安装web容器:

pip install uwsgi

小技巧:如果使用pip install安装库比较慢,可以用豆瓣的镜像,方法类似下面:

pip install django -i http://pypi.douban.com/simple --trusted-host pypi.douban.com

 

创建开源代码库

在github中创建仓库shareditor,并在本地创建空仓库提交

github库在:https://github.com/lcdevelop/shareditor

本地仓库如下:

[lichuang@localhost:~/Developer/shareditor $] ls
README.md
[lichuang@localhost:~/Developer/shareditor $] pwd
/Users/lichuang/Developer/shareditor

 

创建django工程

在安装django时已经自动帮我们安装了django-admin工具,执行如下命令自动创建一个完整的工程目录(其中最后一个参数是工程目录,倒数第二个参数是工程名):

django-admin startproject shareditor /Users/lichuang/Developer/shareditor

这时能够找到自动创建的manage.py文件(一个工具脚本,不需要修改),和工程总目录shareditor(里面包含了配置文件settings.py、总路由配置urls.py、wsgi协议配置文件wsgi.py)

下面我们在这个工程里创建我们网站app:

django-admin startapp web

我们看到它自动创建了web目录,并且自动帮我们组织了一些文件,包括:

admin.py:数据库表的后台管理类一般定义在这里

apps.py:这个app的配置信息,这个文件一般不动

migrations目录:存储数据库迁移相关的临时文件,不需要动

models.py:和数据库对应的model类一般定义在这里

tests.py:自动化脚本

views.py:视图层脚本,我一般会把控制逻辑写到这里

这些文件全都看不懂也没有关系,到现在为止,我们的网站已经可以运行了,执行:

python manage.py runserver

我们可以看到一些提示,直接访问http://127.0.0.1:8000/就可以访问网页了,如下:

上面的页面是django展示的默认页面,下面我们稍作修改来看看django框架是怎么按照我们的指示工作的

 

helloworld

修改web/views.py,增加如下函数:

from django.http import HttpResponse
def index(request):
    return HttpResponse('Hello World!')

这仅仅是定义一个函数,然并卵

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

我们来修改一下我们的路由规则,修改shareditor/urls.py,把内容改成:

from web import views
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', views.index)
]

下面我们重新执行python manage.py runserver,并打开浏览器看看是不是看到了高大上的Hello World!

 

让网站更专业

上面执行的python manage.py runserver实际上只是django的一个用于开发和调试的方法,它只是一个进程一个线程在运行,无法支持网站的高并发访问,下面我们介绍一下如何部署一个专业的网站。

首先我们配置好我们的web容器,在shareditor目录下创建uwsgi.ini,内容如下:

[uwsgi]
chdir = /Users/lichuang/Developer/shareditor
http = 127.0.0.1:8080
http-keepalive = 1
module = shareditor.wsgi:application
master = true
processes = 4
daemonize = /Users/lichuang/Developer/shareditor/logs/uwsgi.log
disable-logging = 1
buffer-size = 16384
harakiri = 5
post-buffering = 8192
post-buffering-bufsize = 65536
pidfile = /Users/lichuang/Developer/shareditor/logs/uwsgi.pid
enable-threads = true
single-interpreter = true

这里的目录要随着你部署的目录做相应修改

因为logs目录还不存在,所以我们手工mkdir创建一个

下面执行启动命令:

uwsgi --ini shareditor/uwsgi.ini

这时我们可以查看一下logs/uwsgi.log文件,如果没有异常信息说明网站已经部署成功了,我们ps ux|grep uwsgi看一下进程:

lichuang 13390   0.4  0.0  2425088    300 s004  S+   10:19下午   0:00.00 grep --color uwsgi
lichuang 13307   0.0  0.0  2491336    924   ??  S    10:18下午   0:00.00 uwsgi --ini shareditor/uwsgi.ini
lichuang 13306   0.0  0.0  2491336   2540   ??  S    10:18下午   0:00.00 uwsgi --ini shareditor/uwsgi.ini
lichuang 13305   0.0  0.0  2491336   2520   ??  S    10:18下午   0:00.00 uwsgi --ini shareditor/uwsgi.ini
lichuang 13304   0.0  0.0  2491336   2484   ??  S    10:18下午   0:00.00 uwsgi --ini shareditor/uwsgi.ini

可以看到启动了4个进程,其中一个守护进程用来接收和分发请求,3个子进程(对应配置文件里的processes = 4)用来处理请求

这时我们打开浏览器访问:http://127.0.0.1:8080/又能看到Hello World!了

 

高可用性部署(新手可略过)

另外为了让我们的网站具有高可用性(高可用就是挂掉一台机器不影响服务),一台机器启动服务还不行,我们至少要部署两台完全对等的web服务来同时提供服务,那么用户在浏览器里访问时到底访问的是哪个机器呢?这里有两种实现方案,一种是配置DNS记录,同一个域名对应多个ip,那么当一个ip不可用时浏览器会自动尝试另外的ip,还有一种方法就是通过稳定的代理服务器(如nginx、apache httpd等)来配置成一个负载均衡代理,对外暴露的一个ip,对内连接到多台web服务器

从0到1搭建个人网站 一-我关于技术栈选择的一点建议

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

聊聊工程

如今,数据科学家已经逐渐取代现在的“软件工程师”成为IT行业的主流职业,和“全民都在聊人工智能”一样,可能全部IT工作者都要天天研究算法、琢磨模型、跑数据、调参数、跑数据、调参数,那些被淘汰的“软件工程师”会真的成为民工一样的行业,但是我觉得任何算法都离不开工程实现,再好的模型没有底层架构的支撑和上层产品应用的展现也无法发挥作用,所以对于一个技术人士,不擅长工程终难把能力发挥到极致。随着中国和国外前沿科技的接轨,将来一定是小创业团队成为主流,一个小创业团队更喜欢算法+工程的全能型人才,跟着我一起学习进步,你将来也许就会是其中一个。

 

聊聊重构

说起重构,很多人都有感慨,因为只有当遇到比较大的问题的时候才会考虑重构,比如技术人员流动大导致代码中风格百出,百花齐放,无用代码一大堆不敢删,奇葩逻辑遍地皆是却没有一行注释、一篇文档。在这种无奈情况下,我们不得已选择了重构,寄希望于解决所有问题,但往往代价比收益高出一个数量级,很多人因为重构被迫出局。但重构这件事情是一件必经之路,任何一个产品从诞生到成熟都会经历几次重构,因为没有人能在最初的时候就预示到最终的逻辑(如果能预示那何必有最初呢),就算像BAT这样成熟的公司,他们内部的系统也是平均两年做一次重构。回过头来说一下我的网站重构的初衷:1)我也是不断成长的,作为一个想做全栈的工程师来说,新思路总想去尝试;2)很多关注我的网友觉得我之前写的教程总有意犹未尽的感觉,希望能深入写一点;3)php终究不是世界上最好的语言(此处可能引发战争),用来用去觉得还是迁移python为好,也和我们的机器学习知识做个融合;

 

技术栈选择

首先说语言。我曾经说过,语言只是工具,每门语言都有它存在的理由,它擅长用在什么地方就用在什么地方,不擅长的不要勉强,不歧视、不在一棵树上吊死、哪个行就上哪个。后端语言我选择python,因为python是社区最活跃的语种之一且呈上升趋势,另外也是大数据与人工智能方向的主流语言。web框架我选择django,因为它更专业更强大,扩展性强,社区也更活跃。前端框架我选择直接用django模板渲染,没有选择angular等前端框架,因为seo不友好

 

服务端容器选择

在tomcat、apache httpd、nginx等web服务器下游,需要部署python的应用服务器容器,我选择uwsgi,它类似于nginx,通过一个守护进程把不同的http请求转交给子进程并发处理,并且支持多线程的方式,性能较高,更重要的,django会自动帮我们生成wsgi的配置,天然对uwsgi友好

 

总结

开篇就讲这么多,主要还是得看后面我的重构过程,咳咳!出发!