보통은 LLM을 첨부터 만들다기 보다는 만들어진 LLM을 가져다가 파인튜닝 정도 해서 사용
LLM 만들기
1. 사전훈련 : 일반적인 언어 능력을 학습, 말을 이어서 할 수 있도록
2. 파인튜닝 : 특정 업무를 가르킨다. 특정 업무에 맡는 데이터셋이 필요함(준비가 어려움, 회사에서 보유한 데이터를 잘 정리)
3. 데이터베이스(+인터넷) 검색 기능 (지식의 범위 확장, 최신 데이터, 정확성 향상 등)
4. 내부적으로 질의를 여러번 하게해서 더 좋은 답변을 하도록 함
사전훈련
1. 훈련 데이터 준비 : 이게 제일 어려움
- 데이터 정제(클리닝), 토큰화(뉴럴이 이해하기 좋은 길이로 토큰화 > 문자를 숫자(코드)로 바꿔서 전달)
2. 데이터 로더 준비 : 뉴럴 네트워크에 분할해서 넣어줌 - 훈련시킬때 전체 데이터를 한번에 집어넣어서 훈련을 하지 않고 쪼개서 넣어줌 - pytorch 활용 : 토큰화된 데이터 > input/target (targer은 input의 다음 단어 ex Harry인경우 Potter ...) > 사이즈/셔플 설정 > 토큰을 주입
3. 뉴럴 네트워크 모델 정의 : LLM에서는 트랜스포머 알고리즘 활용
4. 훈련 - Nvidia GPU면 CUDA
- CPU로 해야한다면 데이터 양을 줄여서 해보기 - epoch 10 = 책을 10번 읽었다
- 2번의 input/target을 받아서 뉴럴 네트워크에 넣어준다. - loss는 대답을 얼마나 잘했는지 평가(줄어들 수록 좋음), optimizer는 대답을 잘하도록 뉴럴 네트워에 있는 가중치들을 업데이트
5. 결과 확인
- 다음에 올 확률이 높은 단어를 뽑고, 뽑은걸 다시 집어서 다음에 올 단어를 찾음 ... 반복
=> getMessage() 메서드를 활용하여 messages.properties, messages_en.properties에서 알맞은 메시지를 불러옴
메시지 적용해보기
messages.properties
label.item=상품
label.item.id=상품 ID
label.item.itemName=상품명
label.item.price=가격
label.item.quantity=수량
label.item.open=판매여부
label.item.regions=판매지역
label.item.itemType=상품종류
label.item.deliveryCode=배송방식
page.items=상품 목록
page.item=상품 상세
page.addItem=상품 등록
page.updateItem=상품 수정
button.save=저장
button.cancel=취소
Thymeleaf로 메시지 불러오기 ( #{...} )
- 메시지 표현식인 #{...}을 사용하면 메시지를 불러올 수 있음
ex)
<h2th:text="#{page.addItem}">상품 등록</h2>
국제화
messages_en.properties
label.item=Item
label.item.id=Item ID
label.item.itemName=Item Name
label.item.price=price
label.item.quantity=quantity
label.item.open=open
label.item.regions=region
label.item.itemType=itemType
label.item.deliveryCode=delivery
page.items=Item List
page.item=Item Detail
page.addItem=Item Add
page.updateItem=Item Update
button.save=Save
button.cancel=Cancel
=> 국제화 적용은 그냥 message_en.properties만 만들어두면 끝난 것임
Spring의 국제화 메시지 선택 방법
- Locale정보를 알아야 messages.properties을 적용할지 messages_en.properties을 적용할지 선택 가능
@GetMapping("/add")public String addForm(Model model){
model.addAttribute("item", new Item()); // form 렌더링(th:object 를 위해 빈 객체 넘기기)return"form/addForm";
}
=> th:object="${item}"으로 해당 <form>에서 사용한 객체를 선택함 (Controller에서 넘긴 빈 Item객체를 선택함)
=> th:field=*{itemName}"을 사용하여 ${item}의 itemName을 적용함 (th:field="${item.itmeName}"과 동일한 내용)
=> th:field=*{itemName}"으로 해당 <input>에 id, name, value속성을 자동으로 추가할 수 있음 => 개발이 간편함
=> value속성에는 실제 Controller에서 넘겨준 Item객체의 값이 들어감 (지금은 빈 객체이므로 value="")
=> <form action="item.html" th:action ... >에서 th:action으로 인해 th:action=""으로 렌더링이 되는데, HTML에서 <form>의 action속성 값이 없는 경우, 해당 페이지에 접근한 URL + method속성 값으로 <form> 내용을 제출함
=> switch-case로 선택적으로 렌더링 가능, * 는 java의 default와 동일
블록
- th:block
- HTML 태그의 속성에 사용되지 않는 Thymeleaf 자체 태그
- 일반적인 반복이 어려운 경우 사용
<th:blockth:each="user : ${users}"><div>
사용자 이름1 <spanth:text="${user.username}"></span>
사용자 나이1 <spanth:text="${user.age}"></span></div><div>
요약 <spanth:text="${user.username} + ' / ' + ${user.age}"></span></div></th:block>
자바스크립트 인라인
- <script th:inline="javascript"> : Javascript에 Thymeleaf를 적용하여 넘어온 값을 편리하게 사용할 수 있도록 도움
<script th:inline="javascript">
@GetMapping("/javascript")public String javascript(Model model){
model.addAttribute("user", new User("userA", 23));
addUser(model);
return"basic/javascript";
}
privatevoidaddUser(Model model){
ArrayList<User> list = new ArrayList<>();
list.add(new User("aaa", 12));
list.add(new User("bbb", 22));
list.add(new User("ccc", 32));
model.addAttribute("users", list);
}
<script>var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿var username2 = /*[[${user.username}]]*/"test username";
//객체var user = [[${user}]];
</script><!-- 자바스크립트 인라인 사용 후 --><scriptth:inline="javascript">var username = [[${user.username}]];
var age = [[${user.age}]];
//자바스크립트 내추럴 템플릿var username2 = /*[[${user.username}]]*/"test username";
//객체var user = [[${user}]];
</script>
=> 결과 소스
<!-- 자바스크립트 인라인 사용 전 -->
<script>
var username = userA;
var age = 23;
//자바스크립트 내추럴 템플릿var username2 = /*userA*/"test username";
//객체var user = BasicController.User(username=userA, age=23);
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script>
var username = "userA";
var age = 23;
//자바스크립트 내추럴 템플릿var username2 = "userA";
//객체var user = {"username":"userA","age":23};
</script>
=> 사용 전 : userA에 " 가 없어서 오류가 발생함 > var username = "[[${user.username}]]"; 직접 " 로 감싸줘야 하는 번거로움
=> 사용 후 : Thymeleaf가 알아서 문자 타입으로 인식하여 " 를 포함시켜 줌, javascript에서 문제가 될 수 있는 문자는 escape 처리도 해줌
model에는 user라는 이름으로 User객체가 담겨있는데,
=> 사용 전 : 그냥 toString으로 생성됨 ( var user = BasicController.User(username=userA, age=23); )
=> 사용 후 : 객체를 json형태로 바꿔줌 ( var user = {"username":"userA","age":23}; )
자바스크립트 인라인 each
<scriptth:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
=> 결과
<!-- 자바스크립트 인라인 each --><script>var user1 = {"username":"aaa","age":12};
var user2 = {"username":"bbb","age":22};
var user3 = {"username":"ccc","age":32};
</script>
템플릿 조각
- th:fragment, th:insert, th:replae
- 웹 페이지들에는 서로 동일한 공통 영역이 많음(상단바, 좌측 nav 등등)
@GetMapping("/fragment")
public String fragment() {
return "template/fragment/fragmentMain";
}
footer.html
<!DOCTYPE html><htmlxmlns:th="http://www.thymeleaf.org"><body><footerth:fragment="copy">
푸터 자리 입니다. --------------------------------------
</footer><footerth:fragment="copyParam (param1, param2)"><p>파라미터 자리 입니다. --------------------------------</p><pth:text="${param1}"></p><pth:text="${param2}"></p></footer></body></html>
fragmentMain.html
<!DOCTYPE html><htmlxmlns:th="http://www.thymeleaf.org"><head><metacharset="UTF-8"><title>Title</title></head><body><h1>부분 포함</h1><h2>부분 포함 insert</h2><divth:insert="~{template/fragment/footer :: copy}"></div><h2>부분 포함 replace</h2><divth:replace="~{template/fragment/footer :: copy}"></div><h2>부분 포함 단순 표현식</h2><divth:replace="template/fragment/footer :: copy"></div><h1>파라미터 사용</h1><divth:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div></body></html>
=> Controller에서 return "template/fragment/fragmentMain"; 을 해서 fragmentMain.html이 생성될 때 footer.html에 템플릿 조각으로 지정된 ( <footer th:fragment="copy"> ~ </footer> )가 fragmentMain.html의 th:insert, th:replace로 들어옴
=> ~{template/fragment/footer :: copy} 의미 : footer.html의 th:fragment="copy"라는 템플릿 조각을 가져옴
th:insert는 해당 태그 안에 템플릿 조각을 삽입
th:replace는 해당 태그를 템플릿 조각으로 대체
=> 이때, fragmentMain의 th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')} 으로
footer의 <footer th:fragment="copyParam (param1, param2)">로 데이터를 넘길 수 있음
템플릿 레이아웃 1
- 템플릿 조각에 해당 페이지가 원하는 정보를 넣어서 사용하기
ex) <head>를 공통으로 하여 템플릿 조각(base.html)으로 가져오는데, 특정 페이지(layoutMain.html)에서는 <head>에 필요한 것을 추가
<htmlxmlns:th="http://www.thymeleaf.org"><headth:fragment="common_header(title,links)"><!-- 교체 --><titleth:replace="${title}">레이아웃 타이틀</title><!-- 공통 --><linkrel="stylesheet"type="text/css"media="all"th:href="@{/css/awesomeapp.css}"><linkrel="shortcut icon"th:href="@{/images/favicon.ico}"><scripttype="text/javascript"th:src="@{/sh/scripts/codebase.js}"></script><!-- 추가 --><th:blockth:replace="${links}" /></head>
layoutMain.html
<!DOCTYPE html><htmlxmlns:th="http://www.thymeleaf.org"><headth:replace="template/layout/base :: common_header(~{::title},~{::link})"><title>메인 타이틀</title><linkrel="stylesheet"th:href="@{/css/bootstrap.min.css}"><linkrel="stylesheet"th:href="@{/themes/smoothness/jquery-ui.css}"></head><body>
메인 컨텐츠
</body></html>
=> 결과
<!DOCTYPE html><html><head><!-- 교체 --><title>메인 타이틀</title><!-- 공통 --><linkrel="stylesheet"type="text/css"media="all"href="/css/awesomeapp.css"><linkrel="shortcut icon"href="/images/favicon.ico"><scripttype="text/javascript"src="/sh/scripts/codebase.js"></script><!-- 추가 --><linkrel="stylesheet"href="/css/bootstrap.min.css"><linkrel="stylesheet"href="/themes/smoothness/jquery-ui.css"></head><body>
메인 컨텐츠
</body></html>
<!DOCTYPE html><htmlth:replace="~{template/layoutExtend/layoutFile :: layout(~{::title},~{::section})}"xmlns:th="http://www.thymeleaf.org"><head><title>메인 페이지 타이틀</title></head><body><section><p>메인 페이지 컨텐츠</p><div>메인 페이지 포함 내용</div></section></body></html>
=> layoutExtendMain의 <html>을 layoutFile의 <hrml>로 대체 => 전체 문서가 layoutFile로 바뀌어 버림
=> 대신 layoutExtendMain의 :: layout(~{::title},~{::section})를 사용하여 <title>과 <section>을 layoutFile의 th:replace로 보내서 대체시킴
- 싱글톤 빈 ClientSingleTonBean에서 PrototypeBean을 주입받아 사용하게 됨
- singleTonClientUsePrototype() 메서드에서 logic()을 두 번 사용하는데, 원하는 동작은 logic() 호출시마다 새로운 프로토타입 빈이 생성되길 원했지만, 그렇게 동작하지 않음 (count2 값이 2 = 기존 프로토타입 빈의 count값을 증가시켰기 때문)
=> 스프링이 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 되는데, 싱글톤 빈은 생성시점에만 의존관계 주입을 받고 변경되지 않기 때문에, 프로토타입 빈이 새로 생성되기는 하지만 싱글톤 빈과 함께 유지되는 문제점이 있음
사용할 때마다 항상 새로운 프로토타입 빈을 생성하는 방법
- Dependency Lookup : 의존관계를 주입받지 않고, 직접 필요한 의존관계를 찾는 것 (싱글톤 빈에서 필요한 프로토타입 빈을 직접 찾아서 받음)
@Component@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)// MyLogger의 가짜 프록시 클래스를 만들어 두고 HTTP request와 상관없이 가짜 프록시 클래스를 미리 주입해둠publicclassMyLogger{
...
}
@Controller@RequiredArgsConstructorpublicclassLogDemoController{
privatefinal MyLogger myLogger; // 가짜 MyLogger 프록시 클래스를 미리 주입받음@RequestMapping("log-demo")@ResponseBodypublic String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger = " + myLogger); // myLogger = hello.core.common.MyLogger@6634cbd4 // 가짜 MyLogger 프록시 클래스
myLogger.setRequestURL(requestURL);
myLogger.log("LogDemoController Test");
logDemoService.logic("testId");
return"OK";
}
}
@Service@RequiredArgsConstructorpublicclassLogDemoService{
privatefinal MyLogger myLogger; // 가짜 MyLogger 프록시 클래스를 미리 주입받음 publicvoidlogic(String testId){
myLogger.log("LogDemoService id = " + testId);
}
}
- @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)를 사용하면 스프링 컨테이너는 CGLIB 바이트코드 조작 라이브러리로 MyLogger가짜 프록시 클래스를 생성해서 빈 등록함
- LogDemoController, LogDemoService에 MyLogger의 가짜 프록시 클래스가 주입됨
- Client에서 실제로 사용하는 요청이 오면 그때 내부에서 진짜 MyLogger빈을 찾아서 위임함
=> Provider, 프록시의 핵심은 진짜 빈 조회를 실제로 필요한 시점까지 지연할 수 있다는 점
=> 아주 간단한 어노테이션 설정으로 가짜 프록시 객체를 만들 수 있는 것이 다형성, DI컨테이너의 큰 장점