tensorflow.keras搭建gan神经网络,可直接运行

文章目录

  • tensorflow.keras搭建gan神经网络,可直接运行
  • 前言
  • 一、tf.keras搭建gan网络大致步骤
  • 二、使用步骤
    • 1.制作tfrecords数据集
    • 2.读入数据
    • 3.搭建gan网络
      • a.搭建generator网络
      • b.搭建discriminator网络
      • c.整合generator,discriminator网络为gan网络
    • 4.complie编译(建立loss和optimizer优化器)
    • 5.训练网络(建立循环)
    • 6.保存网络
    • 7.完整的gans.py(可运行)
  • 参考资料
  • 最后的话

前言

keras是tensorflow的一个高级API库之一,代码简洁,可读性强。本文采用tensorflow.keras来实现gan网络。具体的原理在本文不作过多阐述,只作为一个案例交流

#####keras中文参考文档


正文

一、tf.keras搭建gan网络大致步骤

1.首先我们需要将所有的图像数据装换为tensorflow提供的tfrecords的格式,利用creat_tfrecords.py文件生成即可(这个文件是我原来用作图像分类的标签生成的脚本文件,如果做gan网络不需要将标签也保存)
2.利用生成的tfrecords文件来建立数据集,利用tf.data.TFRecordDataset来进行设置,本文还提供了另一种方法来对tfrecords数据进行获取,但是殊途同归,方法都差不多
3.搭建generator网络
4.搭建discriminator网络,整合为gan网络(需要在gan网络compile之前将discriminator网络设置为不可训练)
5.建立循环体分别训练generator网络和discriminator网络
6.保存网络gan.model

二、使用步骤

1.制作tfrecords数据集

creat_tfrecords.py
默认生成tfrecords位置为 filename_train="./data/train.tfrecords"
终端输入:python creat_tfrecords.py --data [数据集位置]
生成train.tfrecords,也可以自己动手添加验证集和测试集的数据

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import os
from PIL import Image
import randomobjects = ['cat','dog']#'cat'0,'dog'1filename_train="./data/train.tfrecords"
writer_train= tf.python_io.TFRecordWriter(filename_train)tf.app.flags.DEFINE_string('data', 'None', 'where the datas?.')
FLAGS = tf.app.flags.FLAGSif(FLAGS.data == None):os._exit(0)dim = (224,224)
object_path = FLAGS.data
total = os.listdir(object_path)
for index in total:img_path=os.path.join(object_path,index)img=Image.open(img_path)img=img.resize(dim)img_raw=img.tobytes()for i in range(len(objects)):if objects[i] in index:value = ielse:continueexample = tf.train.Example(features=tf.train.Features(feature={'label': tf.train.Feature(int64_list=tf.train.Int64List(value=[value])),'img_raw': tf.train.Feature(bytes_list=tf.train.BytesList(value=[img_raw]))}))print([index,value])writer_train.write(example.SerializeToString())  #序列化为字符串
writer_train.close()

2.读入数据

利用tf.data.TFRecordDataset建立
代码如下:(load_image函数用来作为map的输入,对数据集进行解码),在main函数中调用:
train_datas,iter = dataset_tfrecords(tfrecords_path,use_keras_fit=False)

def load_image(serialized_example):   features={'label': tf.io.FixedLenFeature([], tf.int64),'img_raw' : tf.io.FixedLenFeature([], tf.string)}parsed_example = tf.io.parse_example(serialized_example,features)image = tf.decode_raw(parsed_example['img_raw'],tf.uint8)image = tf.reshape(image,[-1,224,224,3])image = tf.cast(image,tf.float32)*(1./255)label = tf.cast(parsed_example['label'], tf.int32)label = tf.reshape(label,[-1,1])return image,labeldef dataset_tfrecords(tfrecords_path,use_keras_fit=True): #是否使用tf.kerasif use_keras_fit:epochs_data = 1else:epochs_data = epochsdataset = tf.data.TFRecordDataset([tfrecords_path])#这个可以有多个组成[tfrecords_name1,tfrecords_name2,...],可以用os.listdir(tfrecords_path):dataset = dataset\.repeat(epochs_data)\.shuffle(1000)\ .batch(batch_size)\.map(load_image,num_parallel_calls = 2)#注意一定要将shuffle操作放在batch前iter = dataset.make_initializable_iterator()#make_one_shot_iterator()train_datas = iter.get_next() #用train_datas[0],[1]的方式得到值return train_datas,iter

3.搭建gan网络

a.搭建generator网络

    generator = keras.models.Sequential([#fullyconnected netskeras.layers.Dense(256,activation='selu',input_shape=[coding_size]),keras.layers.Dense(64,activation='selu'),keras.layers.Dense(256,activation='selu'),keras.layers.Dense(1024,activation='selu'),keras.layers.Dense(7*7*64,activation='selu'),keras.layers.Reshape([7,7,64]),#7*7*64#反卷积keras.layers.Conv2DTranspose(64,kernel_size=3,strides=2,padding='same',activation='selu'),#14*14*64keras.layers.Conv2DTranspose(64,kernel_size=3,strides=2,padding='same',activation='selu'),#28*28*64keras.layers.Conv2DTranspose(32,kernel_size=3,strides=2,padding='same',activation='selu'),#56*56*32keras.layers.Conv2DTranspose(16,kernel_size=3,strides=2,padding='same',activation='selu'),#112*112*16keras.layers.Conv2DTranspose(3,kernel_size=3,strides=2,padding='same',activation='tanh'),#使用tanh代替sigmoid#224*224*3keras.layers.Reshape([224,224,3])])

b.搭建discriminator网络

    discriminator = keras.models.Sequential([keras.layers.Conv2D(128,kernel_size=3,padding='same',strides=2,activation='selu',input_shape=[224,224,3]),keras.layers.MaxPool2D(pool_size=2),#56*56*128keras.layers.Conv2D(64,kernel_size=3,padding='same',strides=2,activation='selu'),keras.layers.MaxPool2D(pool_size=2),#14*14*64keras.layers.Conv2D(32,kernel_size=3,padding='same',strides=2,activation='selu'),#7*7*32keras.layers.Flatten(),#dropout 0.4keras.layers.Dropout(0.4),keras.layers.Dense(512,activation='selu'),keras.layers.Dropout(0.4),keras.layers.Dense(64,activation='selu'),keras.layers.Dropout(0.4),#the last netkeras.layers.Dense(1,activation='sigmoid')])

c.整合generator,discriminator网络为gan网络

gan = keras.models.Sequential([generator,discriminator])

4.complie编译(建立loss和optimizer优化器)

    #compile the netdiscriminator.compile(loss="binary_crossentropy",optimizer='rmsprop')# metrics=['accuracy'])discriminator.trainable=Falsegan.compile(loss="binary_crossentropy",optimizer='rmsprop')# metrics=['accuracy'])

5.训练网络(建立循环)

获取数据集:

train_datas,iter = dataset_tfrecords(tfrecords_path,use_keras_fit=False)

循环体:(在里面使用cv2来对generator网络查看)

    sess = tf.Session()sess.run(iter.initializer)#打开线程协调器coord = tf.train.Coordinator()threads = tf.train.start_queue_runners(sess=sess,coord=coord)generator,discriminator = gan.layersprint("-----------------start---------------")for step in range(num_steps):try:#get the timestart_time = time.time()#phase 1 - training the discriminatornoise = np.random.normal(size=batch_size*coding_size).reshape([batch_size,coding_size])noise = np.cast[np.float32](noise)generated_images = generator.predict(noise)train_datas_ = sess.run(train_datas)x_fake_and_real = np.concatenate([generated_images,train_datas_[0]],axis = 0)#np.concatenate#千万不能再循环体内用tf.concat,不能用tf相关的函数在循环体内定义#否则内存会被耗尽,而且训练速度越来越慢y1 = np.array([[0.]]*batch_size+[[1.]]*len(train_datas_[0]))discriminator.trainable = Truedis_loss = discriminator.train_on_batch(x_fake_and_real,y1)#将keras 的train_on_batch函数放在gan网络中是明智之举#phase 2 - training the generatornoise = np.random.normal(size=batch_size*coding_size).reshape([batch_size,coding_size])noise = np.cast[np.float32](noise)y2 = np.array([[1.]]*batch_size)discriminator.trainable = Falsead_loss = gan.train_on_batch(noise,y2)duration = time.time()-start_timeif step % 5 == 0:#gan.save_weights('gan.h5')print("The step is %d,discriminator loss:%.3f,adversarial loss:%.3f"%(step,dis_loss,ad_loss),end=' ')print('%.2f s/step'%(duration))if step % 30 == 0 and step != 0:noise = np.random.normal(size=[1,coding_size])noise = np.cast[np.float32](noise)fake_image = generator.predict(noise,steps=1)#复原图像#1.乘以255后需要映射成uint8的类型#2.也可以保持[0,1]的float32类型,依然可以直接输出arr_img = np.array([fake_image],np.float32).reshape([224,224,3])*255arr_img = np.cast[np.uint8](arr_img)#保存为tfrecords用的是PIL.Image,即打开为RGB,所以在用cv显示时需要转换为BGRarr_img = cv2.cvtColor(arr_img,cv2.COLOR_RGB2BGR)cv2.imshow('fake image',arr_img)cv2.waitKey(1500)#show the fake image 1.5scv2.destroyAllWindows()#在迭代完以后会抛出这个错误OutOfRangeError,所以需要将迭代器初始化except tf.errors.OutOfRangeError: sess.run(iter.initializer)#关闭线程协调器coord.request_stop()coord.join(threads)

6.保存网络

 #tensorflow 2.0版本#save the models model_vision = '0001'model_name = 'gans'model_path = os.path.join(model_name,model_name)tf.saved_model.save(gan,model_path)#tensorflow 1.13.1版本#save the models model_vision = '0001'gan.save_weights(model_vision)

7.完整的gans.py(可运行)

# -*- coding: utf-8 -*-
'''@author:zylauthor is zouyuelina Master of Tianjin University(TJU)
'''import tensorflow as tf
from tensorflow import keras
#tf.enable_eager_execution()
import numpy as np
from PIL import Image
import os
import cv2
import timebatch_size = 32
epochs = 120
num_steps = 2000
coding_size = 30
tfrecords_path = 'data/train.tfrecords'#--------------------------------------datasetTfrecord----------------
def load_image(serialized_example):   features={'label': tf.io.FixedLenFeature([], tf.int64),'img_raw' : tf.io.FixedLenFeature([], tf.string)}parsed_example = tf.io.parse_example(serialized_example,features)image = tf.decode_raw(parsed_example['img_raw'],tf.uint8)image = tf.reshape(image,[-1,224,224,3])image = tf.cast(image,tf.float32)*(1./255)label = tf.cast(parsed_example['label'], tf.int32)label = tf.reshape(label,[-1,1])return image,labeldef dataset_tfrecords(tfrecords_path,use_keras_fit=True): #是否使用tf.kerasif use_keras_fit:epochs_data = 1else:epochs_data = epochsdataset = tf.data.TFRecordDataset([tfrecords_path])#这个可以有多个组成[tfrecords_name1,tfrecords_name2,...],可以用os.listdir(tfrecords_path):dataset = dataset\.repeat(epochs_data)\.shuffle(1000)\ .batch(batch_size)\.map(load_image,num_parallel_calls = 2)#注意一定要将shuffle操作放在batch前iter = dataset.make_initializable_iterator()#make_one_shot_iterator()train_datas = iter.get_next() #用train_datas[0],[1]的方式得到值return train_datas,iter#------------------------------------tf.TFRecordReader-----------------
def read_and_decode(tfrecords_path):#根据文件名生成一个队列filename_queue = tf.train.string_input_producer([tfrecords_path],shuffle=True) reader = tf.TFRecordReader()_,  serialized_example = reader.read(filename_queue)features = tf.parse_single_example(serialized_example,features={'label': tf.FixedLenFeature([], tf.int64),'img_raw' : tf.FixedLenFeature([], tf.string)})image = tf.decode_raw(features['img_raw'], tf.uint8)image = tf.reshape(image,[224,224,3])#reshape 200*200*3image = tf.cast(image,tf.float32)*(1./255)#image张量可以除以255,*(1./255)label = tf.cast(features['label'], tf.int32)img_batch, label_batch = tf.train.shuffle_batch([image,label],batch_size=batch_size,num_threads=4,capacity= 640,min_after_dequeue=5)return [img_batch,label_batch]#Autodecode 解码器
def autoencode():encoder = keras.models.Sequential([keras.layers.Conv2D(32,kernel_size=3,padding='same',strides=2,activation='selu',input_shape=[224,224,3]),#112*112*32keras.layers.MaxPool2D(pool_size=2),#56*56*32keras.layers.Conv2D(64,kernel_size=3,padding='same',strides=2,activation='selu'),#28*28*64keras.layers.MaxPool2D(pool_size=2),#14*14*64keras.layers.Conv2D(128,kernel_size=3,padding='same',strides=2,activation='selu'),#7*7*128#反卷积keras.layers.Conv2DTranspose(128,kernel_size=3,strides=2,padding='same',activation='selu'),#14*14*128keras.layers.Conv2DTranspose(64,kernel_size=3,strides=2,padding='same',activation='selu'),#28*28*64keras.layers.Conv2DTranspose(32,kernel_size=3,strides=2,padding='same',activation='selu'),#56*56*32keras.layers.Conv2DTranspose(16,kernel_size=3,strides=2,padding='same',activation='selu'),#112*112*16keras.layers.Conv2DTranspose(3,kernel_size=3,strides=2,padding='same',activation='tanh'),#使用tanh代替sigmoid#224*224*3keras.layers.Reshape([224,224,3])])return encoderdef training_keras():'''卷积和池化输出公式:output_size = (input_size-kernel_size+2*padding)/strides+1keras的反卷积输出计算,一般不用out_padding1.若padding = 'valid':output_size = (input_size - 1)*strides + kernel_size2.若padding = 'same:output_size = input_size * strides'''generator = keras.models.Sequential([#fullyconnected netskeras.layers.Dense(256,activation='selu',input_shape=[coding_size]),keras.layers.Dense(64,activation='selu'),keras.layers.Dense(256,activation='selu'),keras.layers.Dense(1024,activation='selu'),keras.layers.Dense(7*7*64,activation='selu'),keras.layers.Reshape([7,7,64]),#7*7*64#反卷积keras.layers.Conv2DTranspose(64,kernel_size=3,strides=2,padding='same',activation='selu'),#14*14*64keras.layers.Conv2DTranspose(64,kernel_size=3,strides=2,padding='same',activation='selu'),#28*28*64keras.layers.Conv2DTranspose(32,kernel_size=3,strides=2,padding='same',activation='selu'),#56*56*32keras.layers.Conv2DTranspose(16,kernel_size=3,strides=2,padding='same',activation='selu'),#112*112*16keras.layers.Conv2DTranspose(3,kernel_size=3,strides=2,padding='same',activation='tanh'),#使用tanh代替sigmoid#224*224*3keras.layers.Reshape([224,224,3])])discriminator = keras.models.Sequential([keras.layers.Conv2D(128,kernel_size=3,padding='same',strides=2,activation='selu',input_shape=[224,224,3]),keras.layers.MaxPool2D(pool_size=2),#56*56*128keras.layers.Conv2D(64,kernel_size=3,padding='same',strides=2,activation='selu'),keras.layers.MaxPool2D(pool_size=2),#14*14*64keras.layers.Conv2D(32,kernel_size=3,padding='same',strides=2,activation='selu'),#7*7*32keras.layers.Flatten(),#dropout 0.4keras.layers.Dropout(0.4),keras.layers.Dense(512,activation='selu'),keras.layers.Dropout(0.4),keras.layers.Dense(64,activation='selu'),keras.layers.Dropout(0.4),#the last netkeras.layers.Dense(1,activation='sigmoid')])#gans network        gan = keras.models.Sequential([generator,discriminator])#compile the netdiscriminator.compile(loss="binary_crossentropy",optimizer='rmsprop')# metrics=['accuracy'])discriminator.trainable=Falsegan.compile(loss="binary_crossentropy",optimizer='rmsprop')# metrics=['accuracy'])#dataset#train_datas = read_and_decode(tfrecords_path)train_datas,iter = dataset_tfrecords(tfrecords_path,use_keras_fit=False)sess = tf.Session()sess.run(iter.initializer)#打开线程协调器coord = tf.train.Coordinator()threads = tf.train.start_queue_runners(sess=sess,coord=coord)generator,discriminator = gan.layersprint("-----------------start---------------")for step in range(num_steps):try:#get the timestart_time = time.time()#phase 1 - training the discriminatornoise = np.random.normal(size=batch_size*coding_size).reshape([batch_size,coding_size])noise = np.cast[np.float32](noise)generated_images = generator.predict(noise)train_datas_ = sess.run(train_datas)x_fake_and_real = np.concatenate([generated_images,train_datas_[0]],axis = 0)#np.concatenate#千万不能再循环体内用tf.concat,不能用tf相关的函数在循环体内定义#否则内存会被耗尽,而且训练速度越来越慢y1 = np.array([[0.]]*batch_size+[[1.]]*len(train_datas_[0]))discriminator.trainable = Truedis_loss = discriminator.train_on_batch(x_fake_and_real,y1)#将keras 的train_on_batch函数放在gan网络中是明智之举#phase 2 - training the generatornoise = np.random.normal(size=batch_size*coding_size).reshape([batch_size,coding_size])noise = np.cast[np.float32](noise)y2 = np.array([[1.]]*batch_size)discriminator.trainable = Falsead_loss = gan.train_on_batch(noise,y2)duration = time.time()-start_timeif step % 5 == 0:#gan.save_weights('gan.h5')print("The step is %d,discriminator loss:%.3f,adversarial loss:%.3f"%(step,dis_loss,ad_loss),end=' ')print('%.2f s/step'%(duration))if step % 30 == 0 and step != 0:noise = np.random.normal(size=[1,coding_size])noise = np.cast[np.float32](noise)fake_image = generator.predict(noise,steps=1)#复原图像#1.乘以255后需要映射成uint8的类型#2.也可以保持[0,1]的float32类型,依然可以直接输出arr_img = np.array([fake_image],np.float32).reshape([224,224,3])*255arr_img = np.cast[np.uint8](arr_img)#保存为tfrecords用的是PIL.Image,即打开为RGB,所以在用cv显示时需要转换为BGRarr_img = cv2.cvtColor(arr_img,cv2.COLOR_RGB2BGR)cv2.imshow('fake image',arr_img)cv2.waitKey(1500)#show the fake image 1.5scv2.destroyAllWindows()#在迭代完以后会抛出这个错误OutOfRangeError,所以需要将迭代器初始化except tf.errors.OutOfRangeError: sess.run(iter.initializer)#关闭线程协调器coord.request_stop()coord.join(threads)#save the models tf2.0版本使用model_vision = '0001'model_name = 'gans'model_path = os.path.join(model_name,model_name)tf.saved_model.save(gan,model_path)#save the models tensorflow 1.13.1版本model_vision = '0001'gan.save_weights(model_vision)def main():training_keras()
main()

至此便完成了简单的gan训练


参考资料

论文:《Generative Adversarial Networks》
参考源码:
https://github.com/eriklindernoren/Keras-GAN/blob/master/gan/gan.py
参考博客:
https://blog.csdn.net/u010138055/article/details/94441812

最后的话

深度学习、机器学习的学渣小硕一枚,刚起步,不足的地方还请大家多多指教。

tensorflow.keras搭建gan神经网络,可直接运行相关推荐

  1. 30行代码就可以实现看图识字!python使用tensorflow.keras搭建简单神经网络

    文章目录 搭建过程 1. 引入必需的库 2. 引入数据集 3. 搭建神经网络层 4. 编译神经网络模型 5. 训练模型 效果测试 大概几个月前,神经网络.人工智能等概念在我心里仍高不可攀,直到自己亲身 ...

  2. 教你如何用Keras搭建分类神经网络

    摘要:本文主要通过Keras实现了一个分类学习的案例,并详细介绍了MNIST手写体识别数据集. 本文分享自华为云社区<[Python人工智能] 十七.Keras搭建分类神经网络及MNIST数字图 ...

  3. 深度学习系列笔记——贰 (基于Tensorflow Keras搭建的猫狗大战模型 一)

    猫狗大战是著名的竞赛网站kaggle几年前的一个比赛,参赛者得到猫狗各12500张图片,作为训练集,另外还会得到12500张猫和狗的图片,作为验证.最后提交结果至kaggle平台,获得评测分数. 本篇 ...

  4. Keras——用Keras搭建分类神经网络

    文章目录 1.前言 2.用Keras搭建分类神经网络 2.1.导入必要模块 2.2.数据预处理 2.3.搭建模型 2.4.激活模型 2.5.训练+测试 1.前言 今天用 Keras 来构建一个分类神经 ...

  5. Keras——用Keras搭建线性回归神经网络

    文章目录 1.前言 2.用Keras搭建线性回归神经网络 2.1.导入必要模块 2.2.创建数据 2.3.搭建模型 2.4.激活模型 2.5.训练+测试 1.前言 神经网络可以用来模拟回归问题 (re ...

  6. Python-深度学习-学习笔记(13):keras搭建卷积神经网络(对二维数据进行一维卷积)

    Python-深度学习-学习笔记(13):keras搭建卷积神经网络(对二维数据进行一维卷积) 卷积神经网络进行图像分类是深度学习关于图像处理的一个应用,卷积神经网络的优点是能够直接与图像像素进行卷积 ...

  7. 用Keras搭建一个神经网络实现糖尿病检测

    这几天一直在弄导师交代的数据分析任务,从此博客中收到很大启发,原来的博客地址:搭建神经网络 教程概述 这里不需要编写太多的代码,不过我们将一步步慢慢地告诉你怎么以后怎么创建自己的模型. 教程将会涵盖以 ...

  8. 图像去模糊代码 python_用Keras搭建GAN:图像去模糊中的应用(附代码)

    雷锋网 (公众号:雷锋网) 按:本文为 雷锋字幕组 编译的技术博客,原标题GAN with Keras: Application to Image Deblurring,作者为Raphaël Meud ...

  9. cnn神经网络可以用于数据拟合吗_使用Keras搭建卷积神经网络进行手写识别的入门(包含代码解读)...

    本文是发在Medium上的一篇博客:<Handwritten Equation Solver using Convolutional Neural Network>.本文是原文的翻译.这篇 ...

  10. 【记录】本科毕设:基于树莓派的智能小车设计(使用Tensorflow + Keras 搭建CNN卷积神经网络 使用端到端的学习方法训练CNN)

    0 申明 这是本人2020年的本科毕业设计,内容多为毕设论文和答辩内容中挑选.最初的灵感来自于早前看过的一些项目(抱歉时间久远,只记录了这一个,见下),才让我萌生了做个机电(小车动力与驱动)和控制(树 ...

最新文章

  1. 380万播放量,也许是全网最火的机器学习视频
  2. 系列文章|OKR与敏捷(一):瀑布式目标与敏捷的冲突
  3. VTK修炼之道52:图形基本操作进阶_多分辨率策略(模型抽取的三种方法)
  4. 实现一个 DFA 正则表达式引擎 - 4. DFA 的最小化
  5. vue base64图片不显示_技巧 | word中插入的图片显示不完整怎么办?
  6. 新版DevEco不用USB线下载程序
  7. Excel导入导出帮助类
  8. Fedora 17配置ssh及Windows远程连接
  9. [RMQ] [线段树] POJ 3368 Frequent Values
  10. Maven 用Eclipse创建web项目后报错的解决方式
  11. 中国第一家区块链形式化验证公司获种子轮投资
  12. TCP/IP指南(RFC1180)
  13. coji小机器人_WowWee COJI 可编程机器人玩具——也许是我想多了
  14. C++扫雷小游戏(附赠源代码)
  15. webpack-theme-color-replacer 路由跳转之后,样式丢失
  16. Accept CS Ph.D. Offer from Stony Brook University,去SUNY石溪大学的CS Ph.D.啦
  17. 软件加壳的简易实现方式
  18. oCPC实践录 | 广告冷启动问题的思考与总结
  19. r7000p装linux双系统,联想拯救者 刃7000台式机设置u盘启动(支持uefi/bios双启动)
  20. MySQL技术总结第一篇

热门文章

  1. foreach 循环中删除一条数据_SQL Server中删除重复数据的几个方法
  2. vmware+player+12+linux,Vmware player 12
  3. git分支详细讲解,模拟分支开发,为什么使用分支开发
  4. 向视图中插入的数据能进入到基本表中去吗?_数据库调优,调的是什么及常见手法...
  5. 【转】el-table复选框分页记忆-非:reserve-selection=true模式
  6. MaxCompute助力小影短视频走向全球化
  7. QTP自动化测试最佳实践
  8. Inception介绍(MySQL自动化运维工具)
  9. CodeForces 292D Connected Components (并查集+YY)
  10. Todoist Chrome:待办事项列表及任务管理