From dcd26b69f3e19b377865c73edaad64d0097795cb Mon Sep 17 00:00:00 2001 From: Decalogue <1044908508@qq.com> Date: Mon, 29 Jun 2020 10:42:11 +0800 Subject: [PATCH] update --- README.rst | 4 +- ai/__init__.py | 2 - ai/{iconf.py => conf.py} | 11 +- ai/conf/self.conf | 4 - ai/helper/__init__.py | 266 +++++++++++---- ai/helper/face.py | 369 +++++++++++++++++++++ ai/helper/pdf.py | 6 +- ai/helper/self.conf | 12 + ai/helper/vis.py | 28 ++ ai/metric/__init__.py | 2 + ai/nn/__init__.py | 2 + ai/optim/__init__.py | 691 +++++++++++++++++++++++++++++++++++++++ ai/training/__init__.py | 91 ++++++ requirements.txt | 12 + setup.py | 10 +- 15 files changed, 1424 insertions(+), 86 deletions(-) rename ai/{iconf.py => conf.py} (87%) delete mode 100644 ai/conf/self.conf create mode 100644 ai/helper/face.py create mode 100644 ai/helper/self.conf create mode 100644 ai/helper/vis.py create mode 100644 ai/metric/__init__.py create mode 100644 ai/nn/__init__.py create mode 100644 ai/optim/__init__.py create mode 100644 ai/training/__init__.py diff --git a/README.rst b/README.rst index d89a673..94df131 100644 --- a/README.rst +++ b/README.rst @@ -29,12 +29,12 @@ AI Overview ======== -`AI is an artificial intelligence tool lib. AI 是一个人工智能工具库。` +`AI is a simple and easy-to-use artificial intelligence tool library. AI 是一个简单好用的人工智能工具库。` Requirements ============ -* Python 3.3+ +* Python 3.5+ * Works on Linux, Windows, Mac OSX, MIT Install diff --git a/ai/__init__.py b/ai/__init__.py index bbd752d..4c48b5a 100644 --- a/ai/__init__.py +++ b/ai/__init__.py @@ -1,3 +1 @@ # -*- coding: utf-8 -*- - -"""Ai lib.""" diff --git a/ai/iconf.py b/ai/conf.py similarity index 87% rename from ai/iconf.py rename to ai/conf.py index 3ef413b..cffa0cd 100644 --- a/ai/iconf.py +++ b/ai/conf.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +""" ai.conf """ import os import yaml @@ -6,15 +7,15 @@ class AttrDict(dict): - def __init__(self, *args, **kwargs): - super(AttrDict, self).__init__(*args, **kwargs) - self.__dict__ = self + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self def get_yaml(path): """yaml 方式配置 """ - return AttrDict(yaml.load(open(path, 'r'))) + return AttrDict(yaml.load(open(path, 'r'))) def get_conf(path): @@ -66,7 +67,7 @@ def __init__(self, filepath=None): self.path = filepath else: cur_dir = os.path.split(os.path.realpath(__file__))[0] - self.path = os.path.join(cur_dir, "conf", "self.conf") + self.path = os.path.join(cur_dir, "self.conf") self.conf = get_conf(self.path) self.d = Dict() for s in self.conf.sections(): diff --git a/ai/conf/self.conf b/ai/conf/self.conf deleted file mode 100644 index 95eb4ef..0000000 --- a/ai/conf/self.conf +++ /dev/null @@ -1,4 +0,0 @@ -[translate] -apiurl = http://api.fanyi.baidu.com/api/trans/vip/translate -appid = 20160727000025840 -secretKey = 5FIDH6cowY6rG2VVYWFb \ No newline at end of file diff --git a/ai/helper/__init__.py b/ai/helper/__init__.py index f6a6069..90e965f 100644 --- a/ai/helper/__init__.py +++ b/ai/helper/__init__.py @@ -1,27 +1,36 @@ # -*- coding: utf-8 -*- -"""ai.helper""" +""" ai.helper """ +import argparse import os +import re import time import datetime import hashlib import inspect import json +import logging import random import requests import socket import uuid +import numpy as np from collections import Counter, OrderedDict from functools import wraps +from logging import Logger +from logging.handlers import TimedRotatingFileHandler from time import sleep +from sklearn.feature_extraction.text import CountVectorizer +from ai.conf import Config -from ai.iconf import Config, get_yaml - -conf = Config() +cur_dir = os.path.split(os.path.realpath(__file__))[0] +conf = Config(os.path.join(cur_dir, "self.conf")) +not_zh = re.compile(r"[^\u4e00-\u9fa5]") class Error(Exception): - """Base class for exceptions in this module.""" + """ Base class for exceptions in this module. + """ def __init__(self, value): self.value = value @@ -30,16 +39,18 @@ def __str__(self): class StringPatternError(Error): - """Exception raised for errors in the pattern of string args.""" + """ Exception raised for errors in the pattern of string args. + """ pass class JsonEncoder(json.JSONEncoder): - """JsonEncoder - 解决json.dumps不能序列化datetime类型的问题:使用Python自带的json.dumps方法 - 转换数据为json的时候,如果格式化的数据中有datetime类型的数据时会报错。 - TypeError: datetime.datetime(2014, 03, 20, 12, 10, 44) is not JSON serializable - Usage: json.dumps(data, cls=JsonEncoder) + """ JsonEncoder + 解决 json.dumps 不能序列化 datetime 类型的问题:使用 Python 自带的 json.dumps 方法 + 转换数据为 json 格式的时候,如果格式化的数据中有 datetime 类型的数据时将会报错。 + TypeError: datetime.datetime(2014, 03, 20, 12, 10, 44) is not JSON serializable + Usage: + json.dumps(data, cls=JsonEncoder) """ def default(self, obj): if isinstance(obj, datetime.datetime): @@ -50,9 +61,56 @@ def default(self, obj): return json.JSONEncoder.default(self, obj) +def ensure_dir(path): + """ 确保目录存在 """ + if not os.path.exists(path): + os.makedirs(path) + + +def init_logger(log_name, log_dir): + """ 日志模块 + 1. 同时将日志打印到屏幕和文件中 + 2. 默认值保留近30天日志文件 + """ + ensure_dir(log_dir) + if log_name not in Logger.manager.loggerDict: + logger = logging.getLogger(log_name) + logger.setLevel(logging.DEBUG) + handler = TimedRotatingFileHandler( + filename=os.path.join(log_dir, "%s.log" % log_name), + when="D", + backupCount=30, + ) + datefmt = "%Y-%m-%d %H:%M:%S" + format_str = "[%(asctime)s]: %(name)s %(filename)s[line:%(lineno)s] %(levelname)s %(message)s" + formatter = logging.Formatter(format_str, datefmt) + handler.setFormatter(formatter) + handler.setLevel(logging.INFO) + logger.addHandler(handler) + console = logging.StreamHandler() + console.setLevel(logging.INFO) + console.setFormatter(formatter) + logger.addHandler(console) + + handler = TimedRotatingFileHandler( + filename=os.path.join(log_dir, "ERROR.log"), + when="D", + backupCount=30, + ) + datefmt = "%Y-%m-%d %H:%M:%S" + format_str = "[%(asctime)s]: %(name)s %(filename)s[line:%(lineno)s] %(levelname)s %(message)s" + formatter = logging.Formatter(format_str, datefmt) + handler.setFormatter(formatter) + handler.setLevel(logging.ERROR) + logger.addHandler(handler) + logger = logging.getLogger(log_name) + return logger + + def translate_baidu(content, fromLang='zh', toLang='en'): + """ 百度翻译 API """ salt = str(random.randint(32768, 65536)) - sign = conf.translate.appid + content + salt + conf.translate.secretKey + sign = conf.translate.appid + content + salt + conf.translate.secretkey sign = hashlib.md5(sign.encode("utf-8")).hexdigest() try: paramas = { @@ -68,23 +126,12 @@ def translate_baidu(content, fromLang='zh', toLang='en'): res = [d["dst"] for d in data["trans_result"]] return res except Exception as e: - print(content) + print(content, e) return content.split('\n') -def zh2en(content, limit=1): - sleep(limit) - res = translate_baidu(content) - return res - - -def en2zh(content, limit=1): - sleep(limit) - res = translate_baidu(content, fromLang='en', toLang='zh') - return res - - def back_translate_zh(paras, limit=1): + """ 中文回译 """ try: sleep(limit) tmp = translate_baidu('\n'.join(paras), fromLang='zh', toLang='en') @@ -96,6 +143,7 @@ def back_translate_zh(paras, limit=1): def back_translate_en(paras, limit=1): + """ 英文回译 """ try: sleep(limit) tmp = translate_baidu('\n'.join(paras), fromLang='en', toLang='zh') @@ -107,6 +155,8 @@ def back_translate_en(paras, limit=1): def order_dict(d, mode='key'): + """ 对字典按照 key / value 排序 + """ if mode == 'key': res = sorted(d.items(), key=lambda t: t[0]) elif mode == 'value': @@ -116,26 +166,26 @@ def order_dict(d, mode='key'): def get_mac_address(): - """Get mac address. + """ Get mac address. """ mac = uuid.UUID(int=uuid.getnode()).hex[-12:] return ":".join([mac[e:e+2] for e in range(0, 11, 2)]) def get_hostname(): - """Get hostname. + """ Get hostname. """ return socket.getfqdn(socket.gethostname()) def get_ip_address(hostname): - """Get host ip address. + """ Get host ip address. """ return socket.gethostbyname(hostname) def get_host_ip(): - """Get host ip address. + """ Get host ip address. """ try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -147,23 +197,23 @@ def get_host_ip(): def get_current_function_name(): - """Get current function name. + """ Get current function name. """ return inspect.stack()[1][3] class Walk(): - """Walk directory to batch processing. - 遍历目录进行批处理。 + """ Walk directory to batch processing. + 遍历目录进行批处理。 - Subclasses may override the 'handle_file' method to provide custom file processing mode. - 子类可以重写'handle_file'方法来实现自定义的文件处理方式。 + Subclasses may override the 'handle_file' method to provide custom file processing mode. + 子类可以重写 'handle_file' 方法来实现自定义的文件处理方式。 Public attributes: - - filelist: All filenames with full path in directory. - - fnamelist: All filenames in directory. - - dirlist: All dirnames with full path in directory. - - dnamelist: All dirnames in directory. + - filelist: All filenames with full path in directory. + - fnamelist: All filenames in directory. + - dirlist: All dirnames with full path in directory. + - dnamelist: All dirnames in directory. """ def __init__(self): self.filenum = 0 @@ -277,20 +327,20 @@ def handle_file(self, filepath, pattern=None): def time_me(info="used", format_string="ms"): - """Performance analysis - time - - Decorator of time performance analysis. - 性能分析——计时统计 - 系统时间(wall clock time, elapsed time)是指一段程序从运行到终止,系统时钟走过的时间。 - 一般系统时间都是要大于CPU时间的。通常可以由系统提供,在C++/Windows中,可以由提供。 - 注意得到的时间精度是和系统有关系的。 - 1.time.clock()以浮点数计算的秒数返回当前的CPU时间。用来衡量不同程序的耗时,比time.time()更有用。 - time.clock()在不同的系统上含义不同。在UNIX系统上,它返回的是"进程时间",它是用秒表示的浮点数(时间戳)。 - 而在WINDOWS中,第一次调用,返回的是进程运行的实际时间。而第二次之后的调用是自第一次 - 调用以后到现在的运行时间。(实际上是以WIN32上QueryPerformanceCounter()为基础,它比毫秒表示更为精确) - 2.time.perf_counter()能够提供给定平台上精度最高的计时器。计算的仍然是系统时间, - 这会受到许多不同因素的影响,例如机器当前负载。 - 3.time.process_time()提供进程时间。 + """ Performance analysis - time + Decorator of time performance analysis. + 性能分析——计时统计 + + 系统时间(wall clock time, elapsed time)是指一段程序从运行到终止,系统时钟走过的时间。 + 一般系统时间都是要大于 CPU 时间的。通常可以由系统提供,在 C++/Windows 中,可以由 提供。 + 注意得到的时间精度是和系统有关系的。 + 1.time.clock() 以浮点数计算的秒数返回当前的 CPU 时间。用来衡量不同程序的耗时,比 time.time() 更有用。 + time.clock() 在不同的系统上含义不同。在 UNIX 系统上,它返回的是'进程时间',它是用秒表示的浮点数(时间戳)。 + 而在 WINDOWS 中,第一次调用,返回的是进程运行的实际时间。而第二次之后的调用是自第一次调用以后到现在的运行时间。 + (实际上是以 WIN32 上 QueryPerformanceCounter() 为基础,它比毫秒表示更为精确) + 2.time.perf_counter() 能够提供给定平台上精度最高的计时器。计算的仍然是系统时间, + 这会受到许多不同因素的影响,例如机器当前负载。 + 3.time.process_time() 提供进程时间。 Args: info: Customize print info. 自定义提示信息。 @@ -315,7 +365,7 @@ def _wrapper(*args, **kwargs): def get_timestamp(s=None, style='%Y-%m-%d %H:%M:%S', pattern='s'): - """Get timestamp. 获取指定日期表示方式的时间戳或者当前时间戳。 + """ Get timestamp. 获取指定日期表示方式的时间戳或者当前时间戳。 Args: style: Specifies the format of time. 指定日期表示方式。 @@ -335,17 +385,18 @@ def get_timestamp(s=None, style='%Y-%m-%d %H:%M:%S', pattern='s'): def get_current_time(format_string="%Y-%m-%d-%H-%M-%S", info=None): - """Get current time with specific format_string. - 获取指定日期表示方式的当前时间。 + """ Get current time with specific format_string. + 获取指定日期表示方式的当前时间。 + + For Python3 + On Windows, time.strftime() and Unicode characters will raise UnicodeEncodeError. + http://bugs.python.org/issue8304 Args: format_string: Specifies the format of time. 指定日期表示方式。 Defaults to '%Y-%m-%d-%H-%M-%S'. """ assert isinstance(format_string, str), "The format_string must be a string." - # Python3 - # On Windows, time.strftime() and Unicode characters will raise UnicodeEncodeError. - # http://bugs.python.org/issue8304 try: current_time = time.strftime(format_string, time.localtime()) except UnicodeEncodeError: @@ -355,8 +406,8 @@ def get_current_time(format_string="%Y-%m-%d-%H-%M-%S", info=None): def get_age(format_string="%s年%s个月%s天", birthday="2018-1-1"): - """Get age with specific format_string. - 获取指定日期表示方式的年龄。 + """ Get age with specific format_string. + 获取指定日期表示方式的年龄。 Args: format_string: Specifies the format of time. 指定日期表示方式。 @@ -365,7 +416,7 @@ def get_age(format_string="%s年%s个月%s天", birthday="2018-1-1"): assert isinstance(format_string, str), "The format_string must be a string." assert isinstance(birthday, str), "The birthday must be a string." - # 方案1:根据日期字面差计算具体时长 + # 方案1:根据每月具体天数计算具体时长 mdays = [31, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30] # 从12(0)月到11月 ct = get_current_time(format_string="%Y-%m-%d") st = [int(i) for i in birthday.split('-')] @@ -387,7 +438,7 @@ def get_age(format_string="%s年%s个月%s天", birthday="2018-1-1"): month = et[1] + 12 - st[1] - 1 day = et[2] + mdays[(et[1] - 1) % 12] - st[2] - # 方案2:根据日期天数差计算具体时长 + # 方案2:根据每月平均30天计算具体时长 # start_time= datetime.datetime.strptime(birthday, "%Y-%m-%d") # end_time= datetime.datetime.strptime(ct, "%Y-%m-%d") @@ -402,8 +453,8 @@ def get_age(format_string="%s年%s个月%s天", birthday="2018-1-1"): return age -def readlines(filepath, start=0, n=0): - """按行读取从 start 位置开始的指定 n 行,当 n=0 时读取全部 +def readlines(filepath, start=0, n=None): + """ 按行读取从 start 位置开始的指定 n 行,当 n=None 时读取全部 """ with open(filepath, 'r', encoding='UTF-8') as f: if start > 0: @@ -412,7 +463,90 @@ def readlines(filepath, start=0, n=0): cnt = 0 while True: content = f.readline() - if content == '' or (cnt >= n and n != 0): + if content == '' or (cnt >= n and n != None): break cnt += 1 - yield content + yield content + + +class AverageMeter(object): + """ Computes and stores the average and current value + """ + + def __init__(self): + self.reset() + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + +def remove_punc(text): + """ 中文去标点 """ + text = not_zh.sub("", text) + return text + + +def search(pattern, sequence): + """从 sequence 中寻找子串 pattern + 如果找到,返回第一个下标;否则返回-1。 + """ + n = len(pattern) + for i in range(len(sequence)): + if sequence[i:i + n] == pattern: + return i + return -1 + + +def str2bool(v): + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + + +def num_common_words(query, title): + """ 获取公共词的数量 + """ + query = set(query) + title = set(title) + return len(query & title) + + +def jaccard(query, title): + """ 基于离散词的句子 jaccard 距离 + """ + query = set(query) + title = set(title) + intersection_len = len(query & title) + union_len = len(query | title) + return intersection_len / union_len + + +def word_based_similarity(query, title, mode='cosine'): + """ 基于离散词的句子相似度 + """ + try: + counter = CountVectorizer(analyzer='word', token_pattern=u"(?u)\\b\\w+\\b") + counter.fit([query, title]) + result = counter.transform([query, title]).toarray() + vec1, vec2 = result[0], result[1] + if mode == 'euclid': + return np.linalg.norm(vec1 - vec2) + elif mode == 'manhattan': + return np.sum(np.abs(vec1 - vec2)) + else: + return np.sum(vec1 * vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) + except Exception as e: + print('Meet a outlier sample!') + return 0 \ No newline at end of file diff --git a/ai/helper/face.py b/ai/helper/face.py new file mode 100644 index 0000000..47bddd5 --- /dev/null +++ b/ai/helper/face.py @@ -0,0 +1,369 @@ +# -*- coding: utf-8 -*- +""" ai.helper.face """ + +import os +import bz2 +import cv2 +import dlib +import numpy as np +from base64 import b64encode +from pathlib import Path +from PIL import Image, ImageSequence, ImageFont, ImageDraw +from scipy import ndimage +from time import sleep + +from keras.utils import get_file +from aip import AipFace +from ai.helper import ensure_dir +from ai.conf import Config + +cur_dir = os.path.split(os.path.realpath(__file__))[0] +conf = Config(os.path.join(cur_dir, "self.conf")) + +# 人脸检测与属性分析返回说明 https://ai.baidu.com/docs#/Face-Detect-V3/b7203cd6 +APP_ID = str(conf.face_detect.appid) +API_KEY = str(conf.face_detect.apikey) +SECRET_KEY = str(conf.face_detect.secretkey) +client = AipFace(APP_ID, API_KEY, SECRET_KEY) + +img_type = "BASE64" +options = {} +# emotions: 'angry', 'disgust', 'fear', 'happy', 'sad', 'neutral', 'surprise', 'pouty', 'grimace' +options["face_field"] = "emotion" +options["max_face_num"] = 1 +options["face_type"] = "LIVE" +options["liveness_control"] = "LOW" + +landmarks_model_path = unpack_bz2(get_file('shape_predictor_68_face_landmarks.dat.bz2', + conf.landmarks.modelurl, cache_subdir='temp')) +landmarks_detector = LandmarksDetector(landmarks_model_path) + + +def unpack_bz2(src_path): + data = bz2.BZ2File(src_path).read() + dst_path = src_path[:-4] + with open(dst_path, 'wb') as fp: + fp.write(data) + return dst_path + + +def imshow(img_path): + img = cv2.imread(img_path) + cv2.imshow("Image", img) + cv2.waitKey(0) + + +def im2base64(img_path): + with open(img_path, 'rb') as f: + tmp = b64encode(f.read()) + s = tmp.decode() + return s + + +def frame2base64(img_np, fmt='.png'): + img = cv2.imencode(fmt, img_np)[1] + s = str(b64encode(img))[2:-1] + return s + + +def imcompose(imgs, save_path='compose.png', width=256, height=256): + assert len(imgs) >= 1, "The imgs can't be none!" + row, col = len(imgs), len(imgs[0]) + target = Image.new('RGB', (col * width, row * height)) + for i, img_row in enumerate(imgs): + for j, img in enumerate(img_row): + tmp = Image.open(img).resize((width, height), Image.ANTIALIAS) + target.paste(tmp, (j * width, i * height)) + ensure_dir(save_path) + return target.save(save_path) + + +def imresize(img_path, save_dir='.', size=(256, 256)): + name = img_path.rsplit('/', 1)[-1] + im = Image.open(img_path).resize(size, Image.ANTIALIAS) + ensure_dir(save_dir) + im.save(f'{save_dir}/{name}') + + +def imcrop_emo(img_path, threshold=0.9, qps=2, input_size=(256, 256), output_size=(256, 256), save_dir='.', align=False, fmt='.png'): + sleep(1 / qps) + try: + img = cv2.imread(img_path) + if img.shape != input_size: + img = cv2.resize(img, input_size) + except Exception as e: + print(f'{img_path}:', e) + return None + # 方式1 + # img_base64 = im2base64(img_path) + # 方式2 + img_base64 = frame2base64(img) + + res = client.detect(img_base64, img_type, options) + if res['error_code'] != 0: + return res + for face in res['result']['face_list']: + if face['emotion']['probability'] < threshold: + continue + emotion = face['emotion']['type'] + face_token = face['face_token'] + loc = face['location'] + if align: + top = int(loc['top']) + bottom = int(loc['top'] + loc['height']) + left = int(loc['left']) + right = int(loc['left'] + loc['width']) + rotation = loc['rotation'] + crop = img[top:bottom, left:right] + else: + crop = img + if crop.shape != output_size: + crop = cv2.resize(crop, output_size) + ensure_dir(f'{save_dir}/{emotion}') + cv2.imwrite(f'{save_dir}/{emotion}/{face_token}{fmt}', crop) + return res + + +def framecrop_emo(frame, threshold=0.9, qps=2, input_size=(1024, 1024), output_size=(256, 256), save_dir='.', fmt='.png'): + sleep(1 / qps) + img_base64 = frame2base64(frame) + img = cv2.resize(img, input_size) + res = client.detect(img_base64, img_type, options) + if res['error_code'] != 0: + return None + for face in res['result']['face_list']: + if face['emotion']['probability'] < threshold: + continue + emotion = face['emotion']['type'] + face_token = face['face_token'] + loc = face['location'] + + top = int(loc['top']) + bottom = int(loc['top'] + loc['height']) + left = int(loc['left']) + right = int(loc['left'] + loc['width']) + rotation = loc['rotation'] + + crop = frame[top:bottom, left:right] + crop = cv2.resize(crop, output_size) + ensure_dir(f'{save_dir}/{emotion}') + cv2.imwrite(f'{save_dir}/{emotion}/{face_token}{fmt}', crop) + return res + + +def videocrop_emo(video_path, fps=10, input_size=(1024, 1024), output_size=(256, 256), save_dir='.'): + assert os.path.exists(video_path), 'Please make sure video_path exist.' + vc = cv2.VideoCapture(video_path) + cnt = 1 + if vc.isOpened(): + rval, frame = vc.read() + else: + rval = False + while rval: + rval, frame = vc.read() + if cnt % fps == 0: + framecrop(frame, input_size=input_size, output_size=output_size, save_dir=save_dir) + cnt += 1 + vc.release() + + +class LandmarksDetector: + def __init__(self, predictor_model_path): + """ + :param predictor_model_path: path to shape_predictor_68_face_landmarks.dat file + """ + self.detector = dlib.get_frontal_face_detector() # cnn_face_detection_model_v1 also can be used + self.shape_predictor = dlib.shape_predictor(predictor_model_path) + + def get_landmarks(self, image): + img = dlib.load_rgb_image(image) + dets = self.detector(img, 1) + + for detection in dets: + try: + face_landmarks = [(item.x, item.y) for item in self.shape_predictor(img, detection).parts()] + yield face_landmarks + except: + print("Exception in get_landmarks()!") + + +def imalign(src_file, dst_file, face_landmarks, output_size=1024, transform_size=1024, enable_padding=True, x_scale=1, y_scale=1, em_scale=0.1, alpha=False): + """Align function from FFHQ dataset pre-processing step + https://github.com/NVlabs/ffhq-dataset/blob/master/download_ffhq.py + """ + lm = np.array(face_landmarks) + lm_chin = lm[0 : 17] # left-right + lm_eyebrow_left = lm[17 : 22] # left-right + lm_eyebrow_right = lm[22 : 27] # left-right + lm_nose = lm[27 : 31] # top-down + lm_nostrils = lm[31 : 36] # top-down + lm_eye_left = lm[36 : 42] # left-clockwise + lm_eye_right = lm[42 : 48] # left-clockwise + lm_mouth_outer = lm[48 : 60] # left-clockwise + lm_mouth_inner = lm[60 : 68] # left-clockwise + + # Calculate auxiliary vectors. + eye_left = np.mean(lm_eye_left, axis=0) + eye_right = np.mean(lm_eye_right, axis=0) + eye_avg = (eye_left + eye_right) * 0.5 + eye_to_eye = eye_right - eye_left + mouth_left = lm_mouth_outer[0] + mouth_right = lm_mouth_outer[6] + mouth_avg = (mouth_left + mouth_right) * 0.5 + eye_to_mouth = mouth_avg - eye_avg + + # Choose oriented crop rectangle. + x = eye_to_eye - np.flipud(eye_to_mouth) * [-1, 1] + x /= np.hypot(*x) + x *= max(np.hypot(*eye_to_eye) * 2.0, np.hypot(*eye_to_mouth) * 1.8) + x *= x_scale + y = np.flipud(x) * [-y_scale, y_scale] + c = eye_avg + eye_to_mouth * em_scale + quad = np.stack([c - x - y, c - x + y, c + x + y, c + x - y]) + qsize = np.hypot(*x) * 2 + + # Load in-the-wild image. + if not os.path.isfile(src_file): + print('\nCannot find source image. Please run "--wilds" before "--align".') + return + img = Image.open(src_file) + + # Shrink. + shrink = int(np.floor(qsize / output_size * 0.5)) + if shrink > 1: + rsize = (int(np.rint(float(img.size[0]) / shrink)), int(np.rint(float(img.size[1]) / shrink))) + img = img.resize(rsize, Image.ANTIALIAS) + quad /= shrink + qsize /= shrink + + # Crop. + border = max(int(np.rint(qsize * 0.1)), 3) + crop = (int(np.floor(min(quad[:,0]))), int(np.floor(min(quad[:,1]))), int(np.ceil(max(quad[:,0]))), int(np.ceil(max(quad[:,1])))) + crop = (max(crop[0] - border, 0), max(crop[1] - border, 0), min(crop[2] + border, img.size[0]), min(crop[3] + border, img.size[1])) + if crop[2] - crop[0] < img.size[0] or crop[3] - crop[1] < img.size[1]: + img = img.crop(crop) + quad -= crop[0:2] + + # Pad. + pad = (int(np.floor(min(quad[:,0]))), int(np.floor(min(quad[:,1]))), int(np.ceil(max(quad[:,0]))), int(np.ceil(max(quad[:,1])))) + pad = (max(-pad[0] + border, 0), max(-pad[1] + border, 0), max(pad[2] - img.size[0] + border, 0), max(pad[3] - img.size[1] + border, 0)) + if enable_padding and max(pad) > border - 4: + pad = np.maximum(pad, int(np.rint(qsize * 0.3))) + img = np.pad(np.float32(img), ((pad[1], pad[3]), (pad[0], pad[2]), (0, 0)), 'reflect') + h, w, _ = img.shape + y, x, _ = np.ogrid[:h, :w, :1] + mask = np.maximum(1.0 - np.minimum(np.float32(x) / pad[0], np.float32(w-1-x) / pad[2]), 1.0 - np.minimum(np.float32(y) / pad[1], np.float32(h-1-y) / pad[3])) + blur = qsize * 0.02 + img += (ndimage.gaussian_filter(img, [blur, blur, 0]) - img) * np.clip(mask * 3.0 + 1.0, 0.0, 1.0) + img += (np.median(img, axis=(0,1)) - img) * np.clip(mask, 0.0, 1.0) + img = np.uint8(np.clip(np.rint(img), 0, 255)) + if alpha: + mask = 1-np.clip(3.0 * mask, 0.0, 1.0) + mask = np.uint8(np.clip(np.rint(mask*255), 0, 255)) + img = np.concatenate((img, mask), axis=2) + img = Image.fromarray(img, 'RGBA') + else: + img = Image.fromarray(img, 'RGB') + quad += pad[:2] + + # Transform. + img = img.transform((transform_size, transform_size), Image.QUAD, (quad + 0.5).flatten(), Image.BILINEAR) + print(transform_size) + if output_size < transform_size: + img = img.resize((output_size, output_size), Image.ANTIALIAS) + + # Save aligned image. + img.save(dst_file, 'PNG') + + +def align_face(raw_dir, align_dir, min_input_size=(256,256), output_size=1024, transform_size=1024, x_scale=1, y_scale=1, em_scale=0.1, alpha=False): + ensure_dir(align_dir) + for img_name in os.listdir(raw_dir): + print(f'Aligning {img_name} ...') + raw_img_path = os.path.join(raw_dir, img_name) + try: + img = cv2.imread(raw_img_path) + if img.shape[0] < min_input_size[0] or img.shape[1] < min_input_size[1]: + continue + except: + print(f'Read {img_name} error.') + continue + fn = face_img_name = '%s_%02d.png' % (os.path.splitext(img_name)[0], 1) + if os.path.isfile(fn): + continue + try: + print('Getting landmarks...') + for i, face_landmarks in enumerate(landmarks_detector.get_landmarks(raw_img_path), start=1): + try: + print('Starting face alignment...') + face_img_name = '%s_%02d.png' % (os.path.splitext(img_name)[0], i) + aligned_face_path = os.path.join(align_dir, face_img_name) + imalign(raw_img_path, aligned_face_path, face_landmarks, + output_size=output_size, transform_size=transform_size, + x_scale=x_scale, y_scale=y_scale, em_scale=em_scale, alpha=alpha) + print('Wrote result %s' % aligned_face_path) + except: + print("Exception in face alignment!") + except: + print("Exception in landmark detection!") + + +def video2img(infile, save_dir=None): + if not save_dir: + save_dir = infile.rsplit('.', 1)[0] + try: + vc = cv2.VideoCapture(infile) + rval = vc.isOpened() + except: + print("Cant load", infile) + return + if not os.path.exists(save_dir): + os.mkdir(save_dir) + cnt = 0 + rval, frame = vc.read() + while rval: + cv2.imwrite(f'{save_dir}/{cnt}.jpg', frame) + cnt += 1 + rval, frame = vc.read() + print(f"video to {cnt} imgs done") + + +def gif2img(infile, save_dir=None): + if not save_dir: + save_dir = infile.rsplit('.', 1)[0] + try: + im = Image.open(infile) + except IOError: + print("Cant load", infile) + ensure_dir(save_dir) + cnt = 0 + palette = im.getpalette() + try: + while True: + if not im.getpalette(): + im.putpalette(palette) + new_im = Image.new("RGBA", im.size) + new_im.paste(im) + new_im.save(f'{save_dir}/{cnt}.png') + cnt += 1 + im.seek(im.tell() + 1) + except EOFError: + print(f"gif to {cnt} imgs done") + return cnt + + +def fontset(ttf, chars=None, img_size=[256,256], font_size=240, background="black", bg_value=(255,255,255), start_pos=(8,8), mode='scc', save_dir=''): + assert chars != None, 'The chars can not be None.' + font = ImageFont.truetype(ttf, font_size) + font_dir = ttf.rstrip('.ttf') if save_dir == '' else save_dir + ensure_dir(font_dir) + for c in chars: + try: + im = Image.new("RGB", img_size, background) + im_draw = ImageDraw.Draw(im) + im_draw.text(start_pos, c, bg_value, font) + im.save(f"{font_dir}/{c}.png") + except: + print(f'Process {c} error.') + return False + return True \ No newline at end of file diff --git a/ai/helper/pdf.py b/ai/helper/pdf.py index 775f894..f86ee89 100644 --- a/ai/helper/pdf.py +++ b/ai/helper/pdf.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +""" ai.helper.pdf """ + import pdfbox from pathlib import Path @@ -11,8 +13,8 @@ def extract_abstract(filepath, start_tag='Abstract', end_tags=end_tags): - """Extract abstract from a PDF-formatted scientific and technological article - 从 PDF 格式的科技论文中抽取摘要 + """ Extract abstract from a PDF-formatted scientific and technological article + 从 PDF 格式的科技论文中抽取摘要 """ text = p.extract_text(filepath, 'pdf.txt') res = [] diff --git a/ai/helper/self.conf b/ai/helper/self.conf new file mode 100644 index 0000000..c35b5e6 --- /dev/null +++ b/ai/helper/self.conf @@ -0,0 +1,12 @@ +[translate] +apiurl = http://api.fanyi.baidu.com/api/trans/vip/translate +appid = 20160727000025840 +secretkey = 5FIDH6cowY6rG2VVYWFb + +[face_detect] +appid = 17002984 +apikey = P1acGO8kSeIsC6EyNGWrLQ7h +secretkey = PjaGf25YO1TipGV84gBcfEPjVqWK4VrQ + +[landmarks] +modelurl = http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2 \ No newline at end of file diff --git a/ai/helper/vis.py b/ai/helper/vis.py new file mode 100644 index 0000000..725e2fe --- /dev/null +++ b/ai/helper/vis.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" ai.helper.vis """ + +from pyecharts.charts import Bar +from pyecharts.render import make_snapshot +from snapshot_selenium import snapshot + + +def plot_cnt(name, items, values, mode='html', img_path='res.png'): + """ 绘制直方图 + Note: + 从 pyecharts v1 版本开始,add_yaxis 需要传入两个参数,第一个参数为 str 类型 + 关于 snapshot: snapshot-selenium 是 pyecharts + selenium 渲染图片的扩展,使用 selenium 需要配置 browser driver。 + 可以参考 selenium-python 相关介绍,推荐使用 Chrome 浏览器,可以开启 headless 模式。目前支持 Chrome, Safari。 + Download: https://selenium-python.readthedocs.io/installation.html#drivers + Make sure it’s in your PATH, e. g., place it in /usr/bin or /usr/local/bin. + """ + bar = Bar() + bar.add_xaxis(items) + bar.add_yaxis(name, values) + if mode == 'html': + return bar.render() + elif mode == 'notebook': + return bar.render_notebook() + elif mode == 'img': + make_snapshot(snapshot, bar.render(), img_path) + else: + raise ValueError("mode must in ['html', 'notebook', 'img']") \ No newline at end of file diff --git a/ai/metric/__init__.py b/ai/metric/__init__.py new file mode 100644 index 0000000..0b0a5f2 --- /dev/null +++ b/ai/metric/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +""" ai.metric """ diff --git a/ai/nn/__init__.py b/ai/nn/__init__.py new file mode 100644 index 0000000..4826ecb --- /dev/null +++ b/ai/nn/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +""" ai.nn """ diff --git a/ai/optim/__init__.py b/ai/optim/__init__.py new file mode 100644 index 0000000..788879a --- /dev/null +++ b/ai/optim/__init__.py @@ -0,0 +1,691 @@ +# -*- coding: utf-8 -*- +""" ai.optim """ + +import math +import numpy as np +import torch +from torch.optim import Optimizer +from collections import defaultdict + + +def anneal_function(function, step, k, t0, weight): + if function == 'sigmoid': + return float(1 / (1 + np.exp(-k * (step - t0)))) * weight + elif function == 'linear': + return min(1, step / t0) * weight + elif function == 'constant': + return weight + else: + raise ValueError + + +class RecAdamGC(Optimizer): + """ Implementation of RecAdamGC optimizer, a variant of Adam optimizer. + + Parameters: + lr (float): learning rate. Default 1e-3. + betas (tuple of 2 floats): Adams beta parameters (b1, b2). Default: (0.9, 0.999) + eps (float): Adams epsilon. Default: 1e-6 + weight_decay (float): Weight decay. Default: 0.0 + correct_bias (bool): can be set to False to avoid correcting bias in Adam (e.g. like in Bert TF repository). Default True. + anneal_fun (str): a hyperparam for the anneal function, decide the function of the curve. Default 'sigmoid'. + anneal_k (float): a hyperparam for the anneal function, decide the slop of the curve. Choice: [0.05, 0.1, 0.2, 0.5, 1] + anneal_t0 (float): a hyperparam for the anneal function, decide the middle point of the curve. Choice: [100, 250, 500, 1000] + anneal_w (float): a hyperparam for the anneal function, decide the scale of the curve. Default 1.0. + pretrain_cof (float): the coefficient of the quadratic penalty. Default 5000.0. + pretrain_params (list of tensors): the corresponding group of params in the pretrained model. + """ + + def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.0, correct_bias=True, + anneal_fun='sigmoid', anneal_k=0, anneal_t0=0, anneal_w=1.0, pretrain_cof=5000.0, pretrain_params=None): + if lr < 0.0: + raise ValueError("Invalid learning rate: {} - should be >= 0.0".format(lr)) + if not 0.0 <= betas[0] < 1.0: + raise ValueError("Invalid beta parameter: {} - should be in [0.0, 1.0[".format(betas[0])) + if not 0.0 <= betas[1] < 1.0: + raise ValueError("Invalid beta parameter: {} - should be in [0.0, 1.0[".format(betas[1])) + if not 0.0 <= eps: + raise ValueError("Invalid epsilon value: {} - should be >= 0.0".format(eps)) + defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay, correct_bias=correct_bias, + anneal_fun=anneal_fun, anneal_k=anneal_k, anneal_t0=anneal_t0, anneal_w=anneal_w, + pretrain_cof=pretrain_cof, pretrain_params=pretrain_params) + super().__init__(params, defaults) + + def step(self, closure=None): + """Performs a single optimization step. + + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + """ + loss = None + if closure is not None: + loss = closure() + for group in self.param_groups: + for p, pp in zip(group["params"], group["pretrain_params"]): + if p.grad is None: + continue + grad = p.grad.data + if grad.is_sparse: + raise RuntimeError("Adam does not support sparse gradients, please consider SparseAdam instead") + + state = self.state[p] + + # State initialization + if len(state) == 0: + state["step"] = 0 + # Exponential moving average of gradient values + state["exp_avg"] = torch.zeros_like(p.data) + # Exponential moving average of squared gradient values + state["exp_avg_sq"] = torch.zeros_like(p.data) + + exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"] + beta1, beta2 = group["betas"] + + # GC operation for Conv layers and FC layers + length = len(list(p.data.size())) + if length > 1: + grad.add_(-grad.mean(dim=tuple(range(1, length)), keepdim=True)) + + state["step"] += 1 + + # Decay the first and second moment running average coefficient + # In-place operations to update the averages at the same time + exp_avg.mul_(beta1).add_(1.0 - beta1, grad) + exp_avg_sq.mul_(beta2).addcmul_(1.0 - beta2, grad, grad) + denom = exp_avg_sq.sqrt().add_(group["eps"]) + + step_size = group["lr"] + if group["correct_bias"]: # No bias correction for Bert + bias_correction1 = 1.0 - beta1 ** state["step"] + bias_correction2 = 1.0 - beta2 ** state["step"] + step_size = step_size * math.sqrt(bias_correction2) / bias_correction1 + + # With RecAdam method, the optimization objective is + # Loss = lambda(t)*Loss_T + (1-lambda(t))*Loss_S + # Loss = lambda(t)*Loss_T + (1-lambda(t))*\gamma/2*\sum((\theta_i-\theta_i^*)^2) + if group['anneal_w'] > 0.0: + # We calculate the lambda as the annealing function + anneal_lambda = anneal_function(group['anneal_fun'], state["step"], group['anneal_k'], + group['anneal_t0'], group['anneal_w']) + assert anneal_lambda <= group['anneal_w'] + # The loss of the target task is multiplied by lambda(t) + p.data.addcdiv_(-step_size * anneal_lambda, exp_avg, denom) + # Add the quadratic penalty to simulate the pretraining tasks + p.data.add_(-group["lr"] * (group['anneal_w'] - anneal_lambda) * group["pretrain_cof"], p.data - pp.data) + else: + p.data.addcdiv_(-step_size, exp_avg, denom) + + # Just adding the square of the weights to the loss function is *not* + # the correct way of using L2 regularization/weight decay with Adam, + # since that will interact with the m and v parameters in strange ways. + # + # Instead we want to decay the weights in a manner that doesn't interact + # with the m/v parameters. This is equivalent to adding the square + # of the weights to the loss with plain (non-momentum) SGD. + # Add weight decay at the end (fixed version) + if group["weight_decay"] > 0.0: + p.data.add_(-group["lr"] * group["weight_decay"], p.data) + + return loss + + +class Lookahead(Optimizer): + """ + PyTorch implementation of the lookahead wrapper. + Lookahead Optimizer: https://arxiv.org/abs/1907.08610 + """ + def __init__(self, optimizer, k=5, alpha=0.5, pullback_momentum="none"): + ''' + :param optimizer:inner optimizer + :param k (int): number of lookahead steps + :param alpha(float): linear interpolation factor. 1.0 recovers the inner optimizer. + :param pullback_momentum (str): change to inner optimizer momentum on interpolation update + ''' + if not 0.0 <= alpha <= 1.0: + raise ValueError(f'Invalid slow update rate: {alpha}') + if not 1 <= k: + raise ValueError(f'Invalid lookahead steps: {k}') + self.optimizer = optimizer + self.param_groups = self.optimizer.param_groups + self.alpha = alpha + self.k = k + self.step_counter = 0 + assert pullback_momentum in ["reset", "pullback", "none"] + self.pullback_momentum = pullback_momentum + self.state = defaultdict(dict) + + # Cache the current optimizer parameters + for group in self.optimizer.param_groups: + for p in group['params']: + param_state = self.state[p] + param_state['cached_params'] = torch.zeros_like(p.data) + param_state['cached_params'].copy_(p.data) + + def __getstate__(self): + return { + 'state': self.state, + 'optimizer': self.optimizer, + 'alpha': self.alpha, + 'step_counter': self.step_counter, + 'k':self.k, + 'pullback_momentum': self.pullback_momentum + } + + def zero_grad(self): + self.optimizer.zero_grad() + + def state_dict(self): + return self.optimizer.state_dict() + + def load_state_dict(self, state_dict): + self.optimizer.load_state_dict(state_dict) + + def _backup_and_load_cache(self): + """Useful for performing evaluation on the slow weights (which typically generalize better) + """ + for group in self.optimizer.param_groups: + for p in group['params']: + param_state = self.state[p] + param_state['backup_params'] = torch.zeros_like(p.data) + param_state['backup_params'].copy_(p.data) + p.data.copy_(param_state['cached_params']) + + def _clear_and_load_backup(self): + for group in self.optimizer.param_groups: + for p in group['params']: + param_state = self.state[p] + p.data.copy_(param_state['backup_params']) + del param_state['backup_params'] + + def step(self, closure=None): + """Performs a single Lookahead optimization step. + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + """ + loss = self.optimizer.step(closure) + self.step_counter += 1 + + if self.step_counter >= self.k: + self.step_counter = 0 + # Lookahead and cache the current optimizer parameters + for group in self.optimizer.param_groups: + for p in group['params']: + param_state = self.state[p] + p.data.mul_(self.alpha).add_(1.0 - self.alpha, param_state['cached_params']) # crucial line + param_state['cached_params'].copy_(p.data) + if self.pullback_momentum == "pullback": + internal_momentum = self.optimizer.state[p]["momentum_buffer"] + self.optimizer.state[p]["momentum_buffer"] = internal_momentum.mul_(self.alpha).add_( + 1.0 - self.alpha, param_state["cached_mom"]) + param_state["cached_mom"] = self.optimizer.state[p]["momentum_buffer"] + elif self.pullback_momentum == "reset": + self.optimizer.state[p]["momentum_buffer"] = torch.zeros_like(p.data) + + return loss + + +class AdamGC(Optimizer): + r"""Implements AdamGC algorithm with Gradient Centralization. + + It has been proposed in `Adam: A Method for Stochastic Optimization`_. + + Arguments: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups + lr (float, optional): learning rate (default: 1e-3) + betas (Tuple[float, float], optional): coefficients used for computing + running averages of gradient and its square (default: (0.9, 0.999)) + eps (float, optional): term added to the denominator to improve + numerical stability (default: 1e-8) + weight_decay (float, optional): weight decay (L2 penalty) (default: 0) + amsgrad (boolean, optional): whether to use the AMSGrad variant of this + algorithm from the paper `On the Convergence of Adam and Beyond`_ + (default: False) + + .. _Adam\: A Method for Stochastic Optimization: + https://arxiv.org/abs/1412.6980 + .. _On the Convergence of Adam and Beyond: + https://openreview.net/forum?id=ryQu7f-RZ + """ + + def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, + weight_decay=0, amsgrad=False, correct_bias=True): + if not 0.0 <= lr: + raise ValueError("Invalid learning rate: {}".format(lr)) + if not 0.0 <= eps: + raise ValueError("Invalid epsilon value: {}".format(eps)) + if not 0.0 <= betas[0] < 1.0: + raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0])) + if not 0.0 <= betas[1] < 1.0: + raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1])) + defaults = dict(lr=lr, betas=betas, eps=eps, correct_bias=correct_bias, + weight_decay=weight_decay, amsgrad=amsgrad) + super(Adam_GC, self).__init__(params, defaults) + + def __setstate__(self, state): + super(Adam_GC, self).__setstate__(state) + for group in self.param_groups: + group.setdefault('amsgrad', False) + + def step(self, closure=None): + """Performs a single optimization step. + + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + """ + loss = None + if closure is not None: + loss = closure() + + for group in self.param_groups: + for p in group['params']: + if p.grad is None: + continue + grad = p.grad.data + if grad.is_sparse: + raise RuntimeError('Adam does not support sparse gradients, please consider SparseAdam instead') + amsgrad = group['amsgrad'] + + state = self.state[p] + + # State initialization + if len(state) == 0: + state['step'] = 0 + # Exponential moving average of gradient values + state['exp_avg'] = torch.zeros_like(p.data) + # Exponential moving average of squared gradient values + state['exp_avg_sq'] = torch.zeros_like(p.data) + if amsgrad: + # Maintains max of all exp. moving avg. of sq. grad. values + state['max_exp_avg_sq'] = torch.zeros_like(p.data) + + exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq'] + if amsgrad: + max_exp_avg_sq = state['max_exp_avg_sq'] + beta1, beta2 = group['betas'] + + state['step'] += 1 + + if group['weight_decay'] > 0: + grad.add_(group['weight_decay'], p.data) + + # GC operation for Conv layers and FC layers + length = len(list(p.data.size())) + if length > 1: + grad.add_(-grad.mean(dim=tuple(range(1, length)), keepdim=True)) + + # Decay the first and second moment running average coefficient + exp_avg.mul_(beta1).add_(1 - beta1, grad) + exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad) + if amsgrad: + # Maintains the maximum of all 2nd moment running avg. till now + torch.max(max_exp_avg_sq, exp_avg_sq, out=max_exp_avg_sq) + # Use the max. for normalizing running avg. of gradient + denom = (max_exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(group['eps']) + else: + denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(group['eps']) + + step_size = group["lr"] + if group["correct_bias"]: # No bias correction for Bert + bias_correction1 = 1.0 - beta1 ** state["step"] + bias_correction2 = 1.0 - beta2 ** state["step"] + step_size = step_size * math.sqrt(bias_correction2) / bias_correction1 + + p.data.addcdiv_(-step_size, exp_avg, denom) + + return loss + + +class AdamW(Optimizer): + r"""Implements AdamW algorithm. + It has been proposed in `Adam: A Method for Stochastic Optimization`_. + Arguments: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups + lr (float, optional): learning rate (default: 1e-3) + betas (Tuple[float, float], optional): coefficients used for computing + running averages of gradient and its square (default: (0.9, 0.999)) + eps (float, optional): term added to the denominator to improve + numerical stability (default: 1e-8) + weight_decay (float, optional): weight decay (L2 penalty) (default: 0) + amsgrad (boolean, optional): whether to use the AMSGrad variant of this + algorithm from the paper `On the Convergence of Adam and Beyond`_ + .. _Adam\: A Method for Stochastic Optimization: + https://arxiv.org/abs/1412.6980 + .. _On the Convergence of Adam and Beyond: + https://openreview.net/forum?id=ryQu7f-RZ + """ + + def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, + weight_decay=0, amsgrad=False, correct_bias=True): + if not 0.0 <= lr: + raise ValueError("Invalid learning rate: {}".format(lr)) + if not 0.0 <= eps: + raise ValueError("Invalid epsilon value: {}".format(eps)) + if not 0.0 <= betas[0] < 1.0: + raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0])) + if not 0.0 <= betas[1] < 1.0: + raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1])) + defaults = dict(lr=lr, betas=betas, eps=eps, correct_bias=correct_bias, + weight_decay=weight_decay, amsgrad=amsgrad) + super(AdamW, self).__init__(params, defaults) + + def __setstate__(self, state): + super(AdamW, self).__setstate__(state) + for group in self.param_groups: + group.setdefault('amsgrad', False) + + def step(self, closure=None): + """Performs a single optimization step. + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + """ + loss = None + if closure is not None: + loss = closure() + + for group in self.param_groups: + for p in group['params']: + if p.grad is None: + continue + grad = p.grad.data + if grad.is_sparse: + raise RuntimeError('Adam does not support sparse gradients, please consider SparseAdam instead') + amsgrad = group['amsgrad'] + + state = self.state[p] + + # State initialization + if len(state) == 0: + state['step'] = 0 + # Exponential moving average of gradient values + state['exp_avg'] = torch.zeros_like(p.data) + # Exponential moving average of squared gradient values + state['exp_avg_sq'] = torch.zeros_like(p.data) + if amsgrad: + # Maintains max of all exp. moving avg. of sq. grad. values + state['max_exp_avg_sq'] = torch.zeros_like(p.data) + + exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq'] + if amsgrad: + max_exp_avg_sq = state['max_exp_avg_sq'] + beta1, beta2 = group['betas'] + + state['step'] += 1 + + if group['weight_decay'] > 0: + grad.add_(group['weight_decay'], p.data) + + # Decay the first and second moment running average coefficient + exp_avg.mul_(beta1).add_(1 - beta1, grad) + exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad) + if amsgrad: + # Maintains the maximum of all 2nd moment running avg. till now + torch.max(max_exp_avg_sq, exp_avg_sq, out=max_exp_avg_sq) + # Use the max. for normalizing running avg. of gradient + denom = max_exp_avg_sq.sqrt().add_(group['eps']) + else: + denom = exp_avg_sq.sqrt().add_(group['eps']) + + step_size = group["lr"] + if group["correct_bias"]: # No bias correction for Bert + bias_correction1 = 1.0 - beta1 ** state["step"] + bias_correction2 = 1.0 - beta2 ** state["step"] + step_size = step_size * math.sqrt(bias_correction2) / bias_correction1 + + # p.data.addcdiv_(-step_size, exp_avg, denom) + p.data.add_(-step_size, torch.mul(p.data, group['weight_decay']).addcdiv_(1, exp_avg, denom) ) + + return loss + + +class AdamWGC(Optimizer): + r"""Implements AdamWGC algorithm with Gradient Centralization. + It has been proposed in `Adam: A Method for Stochastic Optimization`_. + Arguments: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups + lr (float, optional): learning rate (default: 1e-3) + betas (Tuple[float, float], optional): coefficients used for computing + running averages of gradient and its square (default: (0.9, 0.999)) + eps (float, optional): term added to the denominator to improve + numerical stability (default: 1e-8) + weight_decay (float, optional): weight decay (L2 penalty) (default: 0) + amsgrad (boolean, optional): whether to use the AMSGrad variant of this + algorithm from the paper `On the Convergence of Adam and Beyond`_ + .. _Adam\: A Method for Stochastic Optimization: + https://arxiv.org/abs/1412.6980 + .. _On the Convergence of Adam and Beyond: + https://openreview.net/forum?id=ryQu7f-RZ + """ + + def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, + weight_decay=0, amsgrad=False, correct_bias=True): + if not 0.0 <= lr: + raise ValueError("Invalid learning rate: {}".format(lr)) + if not 0.0 <= eps: + raise ValueError("Invalid epsilon value: {}".format(eps)) + if not 0.0 <= betas[0] < 1.0: + raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0])) + if not 0.0 <= betas[1] < 1.0: + raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1])) + defaults = dict(lr=lr, betas=betas, eps=eps, correct_bias=correct_bias, + weight_decay=weight_decay, amsgrad=amsgrad) + super(AdamW_GC, self).__init__(params, defaults) + + def __setstate__(self, state): + super(AdamW_GC, self).__setstate__(state) + for group in self.param_groups: + group.setdefault('amsgrad', False) + + def step(self, closure=None): + """Performs a single optimization step. + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + """ + loss = None + if closure is not None: + loss = closure() + + for group in self.param_groups: + for p in group['params']: + if p.grad is None: + continue + grad = p.grad.data + if grad.is_sparse: + raise RuntimeError('Adam does not support sparse gradients, please consider SparseAdam instead') + amsgrad = group['amsgrad'] + + state = self.state[p] + + # State initialization + if len(state) == 0: + state['step'] = 0 + # Exponential moving average of gradient values + state['exp_avg'] = torch.zeros_like(p.data) + # Exponential moving average of squared gradient values + state['exp_avg_sq'] = torch.zeros_like(p.data) + if amsgrad: + # Maintains max of all exp. moving avg. of sq. grad. values + state['max_exp_avg_sq'] = torch.zeros_like(p.data) + + exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq'] + if amsgrad: + max_exp_avg_sq = state['max_exp_avg_sq'] + beta1, beta2 = group['betas'] + + # GC operation for Conv layers and FC layers + length = len(list(p.data.size())) + if length > 1: + grad.add_(-grad.mean(dim=tuple(range(1, length)), keepdim=True)) + + state['step'] += 1 + + if group['weight_decay'] > 0: + grad.add_(group['weight_decay'], p.data) + + # Decay the first and second moment running average coefficient + exp_avg.mul_(beta1).add_(1 - beta1, grad) + exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad) + if amsgrad: + # Maintains the maximum of all 2nd moment running avg. till now + torch.max(max_exp_avg_sq, exp_avg_sq, out=max_exp_avg_sq) + # Use the max. for normalizing running avg. of gradient + denom = max_exp_avg_sq.sqrt().add_(group['eps']) + else: + denom = exp_avg_sq.sqrt().add_(group['eps']) + + step_size = group["lr"] + if group["correct_bias"]: # No bias correction for Bert + bias_correction1 = 1.0 - beta1 ** state["step"] + bias_correction2 = 1.0 - beta2 ** state["step"] + step_size = step_size * math.sqrt(bias_correction2) / bias_correction1 + + # p.data.addcdiv_(-step_size, exp_avg, denom) + p.data.add_(-step_size, torch.mul(p.data, group['weight_decay']).addcdiv_(1, exp_avg, denom) ) + + return loss + + +class RangerGC(Optimizer): + + def __init__(self, params, lr=1e-3, k=5, alpha=0.5, n_sma_threshhold=5, betas=(0.95,0.999), + eps=1e-8, weight_decay=0, amsgrad=True, transformer='softplus', smooth=50, + grad_transformer='square', correct_bias=True): + if not 0.0 <= alpha <= 1.0: + raise ValueError(f'Invalid slow update rate: {alpha}') + if not 1 <= k: + raise ValueError(f'Invalid lookahead steps: {k}') + if not lr > 0: + raise ValueError(f'Invalid Learning Rate: {lr}') + if not eps > 0: + raise ValueError(f'Invalid eps: {eps}') + + defaults = dict(lr=lr, k=k, alpha=alpha, step_counter=0, betas=betas, + n_sma_threshhold=n_sma_threshhold, eps=eps, weight_decay=weight_decay, + smooth=smooth, transformer=transformer, grad_transformer=grad_transformer, + amsgrad=amsgrad, correct_bias=correct_bias) + super().__init__(params, defaults) + + # adjustable threshold + self.n_sma_threshhold = n_sma_threshhold + + # lookahead params + self.alpha = alpha + self.k = k + + # radam buffer for state + self.radam_buffer = [[None,None,None] for ind in range(10)] + + def __setstate__(self, state): + super(Ranger, self).__setstate__(state) + + + def step(self, closure=None): + loss = None + if closure is not None: + loss = closure() + + # Evaluate averages and grad, update param tensors + for group in self.param_groups: + + for p in group['params']: + if p.grad is None: + continue + grad = p.grad.data.float() + if grad.is_sparse: + raise RuntimeError('Ranger optimizer does not support sparse gradients') + + amsgrad = group['amsgrad'] + smooth = group['smooth'] + grad_transformer = group['grad_transformer'] + + p_data_fp32 = p.data.float() + + state = self.state[p] + + if len(state) == 0: + state['step'] = 0 + state['exp_avg'] = torch.zeros_like(p_data_fp32) + state['exp_avg_sq'] = torch.zeros_like(p_data_fp32) + if amsgrad: + # Maintains max of all exp. moving avg. of sq. grad. values + state['max_exp_avg_sq'] = torch.zeros_like(p.data) + + # lookahead weight storage now in state dict + state['slow_buffer'] = torch.empty_like(p.data) + state['slow_buffer'].copy_(p.data) + + else: + state['exp_avg'] = state['exp_avg'].type_as(p_data_fp32) + state['exp_avg_sq'] = state['exp_avg_sq'].type_as(p_data_fp32) + + exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq'] + beta1, beta2 = group['betas'] + if amsgrad: + max_exp_avg_sq = state['max_exp_avg_sq'] + + + # compute variance mov avg + exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad) + # compute mean moving avg + exp_avg.mul_(beta1).add_(1 - beta1, grad) + + # transformer + if grad_transformer == 'square': + grad_tmp = grad**2 + elif grad_transformer == 'abs': + grad_tmp = grad.abs() + + exp_avg_sq.mul_(beta2).add_((1 - beta2)*grad_tmp) + + if amsgrad: + # Maintains the maximum of all 2nd moment running avg. till now + torch.max(max_exp_avg_sq, exp_avg_sq, out=max_exp_avg_sq) + # Use the max. for normalizing running avg. of gradient + denomc = max_exp_avg_sq.clone() + else: + denomc = exp_avg_sq.clone() + + if grad_transformer == 'square': + denomc.sqrt_() + + # GC operation for Conv layers and FC layers + length = len(list(p.data.size())) + if length > 1: + grad.add_(-grad.mean(dim=tuple(range(1, length)), keepdim=True)) + + state['step'] += 1 + + if group['weight_decay'] > 0: + p_data_fp32.add_(-group['weight_decay'] * group['lr'], p_data_fp32) + + step_size = group["lr"] + if group["correct_bias"]: # No bias correction for Bert + bias_correction1 = 1.0 - beta1 ** state["step"] + bias_correction2 = 1.0 - beta2 ** state["step"] + step_size = step_size * math.sqrt(bias_correction2) / bias_correction1 + + if group['transformer'] =='softplus': + sp = torch.nn.Softplus( smooth) + denomf = sp( denomc) + p_data_fp32.addcdiv_(-step_size, exp_avg, denomf ) + else: + denom = exp_avg_sq.sqrt().add_(group['eps']) + p_data_fp32.addcdiv_(-step_size * group['lr'], exp_avg, denom) + + p.data.copy_(p_data_fp32) + + # Integrated lookahead... + # We do it at the param level instead of group level + if state['step'] % group['k'] == 0: + slow_p = state['slow_buffer'] # get access to slow param tensor + slow_p.add_(self.alpha, p.data - slow_p) # (fast weights - slow weights) * alpha + p.data.copy_(slow_p) # copy interpolated weights to RAdam param tensor + + return loss \ No newline at end of file diff --git a/ai/training/__init__.py b/ai/training/__init__.py new file mode 100644 index 0000000..918f17b --- /dev/null +++ b/ai/training/__init__.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +""" ai.training """ +import random +import numpy as np +import torch + + +if is_torch_available(): + import torch + from torch.utils.data import TensorDataset + +if is_tf_available(): + import tensorflow as tf + +def set_seed(seed, n_gpu): + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + if n_gpu > 0: + torch.cuda.manual_seed_all(seed) + +def to_list(tensor): + return tensor.detach().cpu().tolist() + + +class FGM(): + def __init__(self, model): + self.model = model + self.backup = {} + + def attack(self, epsilon=1., emb_name='word_embedding.'): + # emb_name 为模型中 embedding 的参数名 + for name, param in self.model.named_parameters(): + if param.requires_grad and emb_name in name: + self.backup[name] = param.data.clone() + norm = torch.norm(param.grad) + if norm != 0 and not torch.isnan(norm): + r_at = epsilon * param.grad / norm + param.data.add_(r_at) + + def restore(self, emb_name='word_embedding.'): + # emb_name 为模型中 embedding 的参数名 + for name, param in self.model.named_parameters(): + if param.requires_grad and emb_name in name: + assert name in self.backup + param.data = self.backup[name] + self.backup = {} + + +class PGD(): + def __init__(self, model, k=3): + self.model = model + self.emb_backup = {} + self.grad_backup = {} + self.k = k + + def attack(self, epsilon=1., alpha=0.33, emb_name='word_embedding.', is_first_attack=False): + # emb_name 为模型中 embedding 的参数名 + for name, param in self.model.named_parameters(): + if param.requires_grad and emb_name in name: + if is_first_attack: + self.emb_backup[name] = param.data.clone() + norm = torch.norm(param.grad) + if norm != 0 and not torch.isnan(norm): + r_at = alpha * param.grad / norm + param.data.add_(r_at) + param.data = self.project(name, param.data, epsilon) + + def restore(self, emb_name='word_embedding.'): + # emb_name 为模型中 embedding 的参数名 + for name, param in self.model.named_parameters(): + if param.requires_grad and emb_name in name: + assert name in self.emb_backup + param.data = self.emb_backup[name] + self.emb_backup = {} + + def project(self, param_name, param_data, epsilon): + r = param_data - self.emb_backup[param_name] + if torch.norm(r) > epsilon: + r = epsilon * r / torch.norm(r) + return param_data + r + + def backup_grad(self): + for name, param in self.model.named_parameters(): + if param.requires_grad: + self.grad_backup[name] = param.grad + + def restore_grad(self): + for name, param in self.model.named_parameters(): + if param.requires_grad: + param.grad = self.grad_backup[name] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e69de29..521ac06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,12 @@ +tqdm +pyecharts +snapshot_selenium +dlib +scipy +keras +tensorflow-gpu +pytorch +torchvision +opencv-python +baidu-aip +Pillow \ No newline at end of file diff --git a/setup.py b/setup.py index ddad2ae..b9ea867 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import sys from setuptools import setup, find_packages -version = "0.0.1" +version = "0.0.3" if sys.argv[-1] == 'tag': os.system("git tag -a %s -m 'version %s'" % (version, version)) @@ -13,8 +13,7 @@ # os.system("python setup.py sdist upload") # os.system("python setup.py bdist_wheel upload") os.system("python setup.py sdist") - # os.system("python setup.py bdist_wheel") - os.system("twine upload dist\*") + os.system("twine upload dist/*") sys.exit() elif sys.argv[-1] == 'test': test_requirements = [ @@ -52,8 +51,9 @@ "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Operating System :: Other OS", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6" + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8" ] )